1018 lines
32 KiB
C++
1018 lines
32 KiB
C++
/*
|
|
* This program source code file is part of KiCad, a free EDA CAD application.
|
|
*
|
|
* Copyright (C) 2023 <author>
|
|
* Copyright (C) 2023 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 3 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, see <http://www.gnu.org/licenses/>.
|
|
*/
|
|
#include <wx/string.h>
|
|
#include <wx/debug.h>
|
|
#include <wx/grid.h>
|
|
#include <common.h>
|
|
#include <widgets/wx_grid.h>
|
|
#include <sch_reference_list.h>
|
|
#include <schematic_settings.h>
|
|
#include "string_utils.h"
|
|
|
|
#include "fields_data_model.h"
|
|
|
|
|
|
const wxString FIELDS_EDITOR_GRID_DATA_MODEL::QUANTITY_VARIABLE = wxS( "${QUANTITY}" );
|
|
const wxString FIELDS_EDITOR_GRID_DATA_MODEL::ITEM_NUMBER_VARIABLE = wxS( "${ITEM_NUMBER}" );
|
|
|
|
|
|
void FIELDS_EDITOR_GRID_DATA_MODEL::AddColumn( const wxString& aFieldName, const wxString& aLabel,
|
|
bool aAddedByUser )
|
|
{
|
|
// Don't add a field twice
|
|
if( GetFieldNameCol( aFieldName ) != -1 )
|
|
return;
|
|
|
|
m_cols.push_back( { aFieldName, aLabel, aAddedByUser, false, false } );
|
|
|
|
for( unsigned i = 0; i < m_symbolsList.GetCount(); ++i )
|
|
{
|
|
if( SCH_SYMBOL* symbol = m_symbolsList[i].GetSymbol() )
|
|
updateDataStoreSymbolField( *symbol, aFieldName );
|
|
}
|
|
}
|
|
|
|
|
|
void FIELDS_EDITOR_GRID_DATA_MODEL::updateDataStoreSymbolField( const SCH_SYMBOL& aSymbol,
|
|
const wxString& aFieldName )
|
|
{
|
|
if( isAttribute( aFieldName ) )
|
|
m_dataStore[aSymbol.m_Uuid][aFieldName] = getAttributeValue( aSymbol, aFieldName );
|
|
else if( const SCH_FIELD* field = aSymbol.GetFieldByName( aFieldName ) )
|
|
m_dataStore[aSymbol.m_Uuid][aFieldName] = field->GetText();
|
|
// Handle fields with variables as names that are not present in the symbol
|
|
// by giving them the correct value
|
|
else if( IsTextVar( aFieldName ) )
|
|
m_dataStore[aSymbol.m_Uuid][aFieldName] = aFieldName;
|
|
else
|
|
m_dataStore[aSymbol.m_Uuid][aFieldName] = wxEmptyString;
|
|
}
|
|
|
|
|
|
void FIELDS_EDITOR_GRID_DATA_MODEL::RemoveColumn( int aCol )
|
|
{
|
|
for( unsigned i = 0; i < m_symbolsList.GetCount(); ++i )
|
|
{
|
|
if( SCH_SYMBOL* symbol = m_symbolsList[i].GetSymbol() )
|
|
m_dataStore[symbol->m_Uuid].erase( m_cols[aCol].m_fieldName );
|
|
}
|
|
|
|
m_cols.erase( m_cols.begin() + aCol );
|
|
}
|
|
|
|
|
|
void FIELDS_EDITOR_GRID_DATA_MODEL::RenameColumn( int aCol, const wxString& newName )
|
|
{
|
|
for( unsigned i = 0; i < m_symbolsList.GetCount(); ++i )
|
|
{
|
|
SCH_SYMBOL* symbol = m_symbolsList[i].GetSymbol();
|
|
|
|
auto node = m_dataStore[symbol->m_Uuid].extract( m_cols[aCol].m_fieldName );
|
|
node.key() = newName;
|
|
m_dataStore[symbol->m_Uuid].insert( std::move( node ) );
|
|
}
|
|
|
|
m_cols[aCol].m_fieldName = newName;
|
|
m_cols[aCol].m_label = newName;
|
|
}
|
|
|
|
|
|
int FIELDS_EDITOR_GRID_DATA_MODEL::GetFieldNameCol( wxString aFieldName )
|
|
{
|
|
for( size_t i = 0; i < m_cols.size(); i++ )
|
|
{
|
|
if( m_cols[i].m_fieldName == aFieldName )
|
|
return (int) i;
|
|
}
|
|
|
|
return -1;
|
|
}
|
|
|
|
|
|
const std::vector<BOM_FIELD> FIELDS_EDITOR_GRID_DATA_MODEL::GetFieldsOrdered()
|
|
{
|
|
std::vector<BOM_FIELD> fields;
|
|
|
|
for( const DATA_MODEL_COL& col : m_cols )
|
|
fields.push_back( { col.m_fieldName, col.m_label, col.m_show, col.m_group } );
|
|
|
|
return fields;
|
|
}
|
|
|
|
|
|
void FIELDS_EDITOR_GRID_DATA_MODEL::SetFieldsOrder( const std::vector<wxString>& aNewOrder )
|
|
{
|
|
size_t foundCount = 0;
|
|
|
|
for( const wxString& newField : aNewOrder )
|
|
{
|
|
for( size_t i = 0; i < m_cols.size(); i++ )
|
|
{
|
|
if( m_cols[i].m_fieldName == newField )
|
|
{
|
|
std::swap( m_cols[foundCount], m_cols[i] );
|
|
foundCount++;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
wxString FIELDS_EDITOR_GRID_DATA_MODEL::GetValue( int aRow, int aCol )
|
|
{
|
|
if( ColIsReference( aCol ) )
|
|
{
|
|
// Poor-man's tree controls
|
|
if( m_rows[aRow].m_Flag == GROUP_COLLAPSED )
|
|
return wxT( "> " ) + GetValue( m_rows[aRow], aCol );
|
|
else if( m_rows[aRow].m_Flag == GROUP_EXPANDED )
|
|
return wxT( "v " ) + GetValue( m_rows[aRow], aCol );
|
|
else if( m_rows[aRow].m_Flag == CHILD_ITEM )
|
|
return wxT( " " ) + GetValue( m_rows[aRow], aCol );
|
|
else
|
|
return wxT( " " ) + GetValue( m_rows[aRow], aCol );
|
|
}
|
|
else
|
|
{
|
|
return GetValue( m_rows[aRow], aCol );
|
|
}
|
|
}
|
|
|
|
|
|
wxString FIELDS_EDITOR_GRID_DATA_MODEL::GetValue( const DATA_MODEL_ROW& group, int aCol,
|
|
const wxString& refDelimiter,
|
|
const wxString& refRangeDelimiter,
|
|
bool resolveVars )
|
|
{
|
|
std::vector<SCH_REFERENCE> references;
|
|
wxString fieldValue;
|
|
|
|
for( const SCH_REFERENCE& ref : group.m_Refs )
|
|
{
|
|
if( ColIsReference( aCol ) || ColIsQuantity( aCol ) || ColIsItemNumber( aCol ) )
|
|
{
|
|
references.push_back( ref );
|
|
}
|
|
else // Other columns are either a single value or ROW_MULTI_ITEMS
|
|
{
|
|
const KIID& symbolID = ref.GetSymbol()->m_Uuid;
|
|
|
|
if( !m_dataStore.count( symbolID )
|
|
|| !m_dataStore[symbolID].count( m_cols[aCol].m_fieldName ) )
|
|
{
|
|
return INDETERMINATE_STATE;
|
|
}
|
|
|
|
wxString refFieldValue;
|
|
|
|
// Only resolve vars on actual variables, otherwise we want to get
|
|
// our values out of the datastore so we can show/export un-applied values
|
|
if( resolveVars
|
|
&& ( IsTextVar( m_cols[aCol].m_fieldName )
|
|
|| IsTextVar( m_dataStore[symbolID][m_cols[aCol].m_fieldName] ) ) )
|
|
{
|
|
refFieldValue = getFieldShownText( ref, m_cols[aCol].m_fieldName );
|
|
}
|
|
else
|
|
refFieldValue = m_dataStore[symbolID][m_cols[aCol].m_fieldName];
|
|
|
|
if( &ref == &group.m_Refs.front() )
|
|
fieldValue = refFieldValue;
|
|
else if( fieldValue != refFieldValue )
|
|
return INDETERMINATE_STATE;
|
|
}
|
|
}
|
|
|
|
if( ColIsReference( aCol ) || ColIsQuantity( aCol ) || ColIsItemNumber( aCol ) )
|
|
{
|
|
// Remove duplicates (other units of multi-unit parts)
|
|
std::sort( references.begin(), references.end(),
|
|
[]( const SCH_REFERENCE& l, const SCH_REFERENCE& r ) -> bool
|
|
{
|
|
wxString l_ref( l.GetRef() << l.GetRefNumber() );
|
|
wxString r_ref( r.GetRef() << r.GetRefNumber() );
|
|
return StrNumCmp( l_ref, r_ref, true ) < 0;
|
|
} );
|
|
|
|
auto logicalEnd = std::unique( references.begin(), references.end(),
|
|
[]( const SCH_REFERENCE& l, const SCH_REFERENCE& r ) -> bool
|
|
{
|
|
// If unannotated then we can't tell what units belong together
|
|
// so we have to leave them all
|
|
if( l.GetRefNumber() == wxT( "?" ) )
|
|
return false;
|
|
|
|
wxString l_ref( l.GetRef() << l.GetRefNumber() );
|
|
wxString r_ref( r.GetRef() << r.GetRefNumber() );
|
|
return l_ref == r_ref;
|
|
} );
|
|
|
|
references.erase( logicalEnd, references.end() );
|
|
}
|
|
|
|
if( ColIsReference( aCol ) )
|
|
fieldValue = SCH_REFERENCE_LIST::Shorthand( references, refDelimiter, refRangeDelimiter );
|
|
else if( ColIsQuantity( aCol ) )
|
|
fieldValue = wxString::Format( wxT( "%d" ), (int) references.size() );
|
|
else if( ColIsItemNumber( aCol ) && group.m_Flag != CHILD_ITEM )
|
|
fieldValue = wxString::Format( wxT( "%d" ), group.m_ItemNumber );
|
|
|
|
return fieldValue;
|
|
}
|
|
|
|
|
|
void FIELDS_EDITOR_GRID_DATA_MODEL::SetValue( int aRow, int aCol, const wxString& aValue )
|
|
{
|
|
wxCHECK_RET( aCol >= 0 && aCol < (int) m_cols.size(), wxS( "Invalid column number" ) );
|
|
|
|
// Can't modify references or text variables column, e.g. ${QUANTITY}
|
|
if( ColIsReference( aCol )
|
|
|| ( IsTextVar( m_cols[aCol].m_fieldName ) && !ColIsAttribute( aCol ) ) )
|
|
{
|
|
return;
|
|
}
|
|
|
|
DATA_MODEL_ROW& rowGroup = m_rows[aRow];
|
|
|
|
for( const SCH_REFERENCE& ref : rowGroup.m_Refs )
|
|
m_dataStore[ref.GetSymbol()->m_Uuid][m_cols[aCol].m_fieldName] = aValue;
|
|
|
|
m_edited = true;
|
|
}
|
|
|
|
|
|
bool FIELDS_EDITOR_GRID_DATA_MODEL::ColIsReference( int aCol )
|
|
{
|
|
wxCHECK( aCol >= 0 && aCol < (int) m_cols.size(), false );
|
|
return m_cols[aCol].m_fieldName == GetCanonicalFieldName( REFERENCE_FIELD );
|
|
}
|
|
|
|
|
|
bool FIELDS_EDITOR_GRID_DATA_MODEL::ColIsValue( int aCol )
|
|
{
|
|
wxCHECK( aCol >= 0 && aCol < (int) m_cols.size(), false );
|
|
return m_cols[aCol].m_fieldName == GetCanonicalFieldName( VALUE_FIELD );
|
|
}
|
|
|
|
|
|
bool FIELDS_EDITOR_GRID_DATA_MODEL::ColIsQuantity( int aCol )
|
|
{
|
|
wxCHECK( aCol >= 0 && aCol < (int) m_cols.size(), false );
|
|
return m_cols[aCol].m_fieldName == QUANTITY_VARIABLE;
|
|
}
|
|
|
|
|
|
bool FIELDS_EDITOR_GRID_DATA_MODEL::ColIsItemNumber( int aCol )
|
|
{
|
|
wxCHECK( aCol >= 0 && aCol < (int) m_cols.size(), false );
|
|
return m_cols[aCol].m_fieldName == ITEM_NUMBER_VARIABLE;
|
|
}
|
|
|
|
|
|
bool FIELDS_EDITOR_GRID_DATA_MODEL::ColIsAttribute( int aCol )
|
|
{
|
|
wxCHECK( aCol >= 0 && aCol < (int) m_cols.size(), false );
|
|
return isAttribute( m_cols[aCol].m_fieldName );
|
|
}
|
|
|
|
|
|
bool FIELDS_EDITOR_GRID_DATA_MODEL::cmp( const DATA_MODEL_ROW& lhGroup,
|
|
const DATA_MODEL_ROW& rhGroup,
|
|
FIELDS_EDITOR_GRID_DATA_MODEL* dataModel, int sortCol,
|
|
bool ascending )
|
|
{
|
|
// Empty rows always go to the bottom, whether ascending or descending
|
|
if( lhGroup.m_Refs.size() == 0 )
|
|
return true;
|
|
else if( rhGroup.m_Refs.size() == 0 )
|
|
return false;
|
|
|
|
// N.B. To meet the iterator sort conditions, we cannot simply invert the truth
|
|
// to get the opposite sort. i.e. ~(a<b) != (a>b)
|
|
auto local_cmp =
|
|
[ ascending ]( const auto a, const auto b )
|
|
{
|
|
if( ascending )
|
|
return a < b;
|
|
else
|
|
return a > b;
|
|
};
|
|
|
|
// Primary sort key is sortCol; secondary is always REFERENCE (column 0)
|
|
|
|
wxString lhs = dataModel->GetValue( lhGroup, sortCol ).Trim( true ).Trim( false );
|
|
wxString rhs = dataModel->GetValue( rhGroup, sortCol ).Trim( true ).Trim( false );
|
|
|
|
if( lhs == rhs || sortCol == REFERENCE_FIELD )
|
|
{
|
|
wxString lhRef = lhGroup.m_Refs[0].GetRef() + lhGroup.m_Refs[0].GetRefNumber();
|
|
wxString rhRef = rhGroup.m_Refs[0].GetRef() + rhGroup.m_Refs[0].GetRefNumber();
|
|
return local_cmp( StrNumCmp( lhRef, rhRef, true ), 0 );
|
|
}
|
|
else
|
|
{
|
|
return local_cmp( ValueStringCompare( lhs, rhs ), 0 );
|
|
}
|
|
}
|
|
|
|
|
|
void FIELDS_EDITOR_GRID_DATA_MODEL::Sort()
|
|
{
|
|
CollapseForSort();
|
|
|
|
// We're going to sort the rows based on their first reference, so the first reference
|
|
// had better be the lowest one.
|
|
for( DATA_MODEL_ROW& row : m_rows )
|
|
{
|
|
std::sort( row.m_Refs.begin(), row.m_Refs.end(),
|
|
[]( const SCH_REFERENCE& lhs, const SCH_REFERENCE& rhs )
|
|
{
|
|
wxString lhs_ref( lhs.GetRef() << lhs.GetRefNumber() );
|
|
wxString rhs_ref( rhs.GetRef() << rhs.GetRefNumber() );
|
|
return StrNumCmp( lhs_ref, rhs_ref, true ) < 0;
|
|
} );
|
|
}
|
|
|
|
std::sort( m_rows.begin(), m_rows.end(),
|
|
[this]( const DATA_MODEL_ROW& lhs, const DATA_MODEL_ROW& rhs ) -> bool
|
|
{
|
|
return cmp( lhs, rhs, this, m_sortColumn, m_sortAscending );
|
|
} );
|
|
|
|
// Time to renumber the item numbers
|
|
int itemNumber = 1;
|
|
for( DATA_MODEL_ROW& row : m_rows )
|
|
{
|
|
row.m_ItemNumber = itemNumber++;
|
|
}
|
|
|
|
ExpandAfterSort();
|
|
}
|
|
|
|
|
|
bool FIELDS_EDITOR_GRID_DATA_MODEL::unitMatch( const SCH_REFERENCE& lhRef,
|
|
const SCH_REFERENCE& rhRef )
|
|
{
|
|
// If items are unannotated then we can't tell if they're units of the same symbol or not
|
|
if( lhRef.GetRefNumber() == wxT( "?" ) )
|
|
return false;
|
|
|
|
return ( lhRef.GetRef() == rhRef.GetRef() && lhRef.GetRefNumber() == rhRef.GetRefNumber() );
|
|
}
|
|
|
|
|
|
bool FIELDS_EDITOR_GRID_DATA_MODEL::groupMatch( const SCH_REFERENCE& lhRef,
|
|
const SCH_REFERENCE& rhRef )
|
|
|
|
{
|
|
int refCol = GetFieldNameCol( GetCanonicalFieldName( REFERENCE_FIELD ) );
|
|
bool matchFound = false;
|
|
|
|
if( refCol == -1 )
|
|
return false;
|
|
|
|
// First check the reference column. This can be done directly out of the
|
|
// SCH_REFERENCEs as the references can't be edited in the grid.
|
|
if( m_cols[refCol].m_group )
|
|
{
|
|
// if we're grouping by reference, then only the prefix must match
|
|
if( lhRef.GetRef() != rhRef.GetRef() )
|
|
return false;
|
|
|
|
matchFound = true;
|
|
}
|
|
|
|
const KIID& lhRefID = lhRef.GetSymbol()->m_Uuid;
|
|
const KIID& rhRefID = rhRef.GetSymbol()->m_Uuid;
|
|
|
|
// Now check all the other columns.
|
|
for( size_t i = 0; i < m_cols.size(); ++i )
|
|
{
|
|
//Handled already
|
|
if( (int) i == refCol )
|
|
continue;
|
|
|
|
if( !m_cols[i].m_group )
|
|
continue;
|
|
|
|
// If the field is a variable, we need to resolve it through the symbol
|
|
// to get the actual current value, otherwise we need to pull it out of the
|
|
// store so the refresh can regroup based on values that haven't been applied
|
|
// to the schematic yet.
|
|
wxString lh, rh;
|
|
|
|
if( IsTextVar( m_cols[i].m_fieldName )
|
|
|| IsTextVar( m_dataStore[lhRefID][m_cols[i].m_fieldName] ) )
|
|
{
|
|
lh = getFieldShownText( lhRef, m_cols[i].m_fieldName );
|
|
}
|
|
else
|
|
lh = m_dataStore[lhRefID][m_cols[i].m_fieldName];
|
|
|
|
if( IsTextVar( m_cols[i].m_fieldName )
|
|
|| IsTextVar( m_dataStore[rhRefID][m_cols[i].m_fieldName] ) )
|
|
{
|
|
rh = getFieldShownText( rhRef, m_cols[i].m_fieldName );
|
|
}
|
|
else
|
|
rh = m_dataStore[rhRefID][m_cols[i].m_fieldName];
|
|
|
|
wxString fieldName = m_cols[i].m_fieldName;
|
|
|
|
if( lh != rh )
|
|
return false;
|
|
|
|
matchFound = true;
|
|
}
|
|
|
|
return matchFound;
|
|
}
|
|
|
|
|
|
wxString FIELDS_EDITOR_GRID_DATA_MODEL::getFieldShownText( const SCH_REFERENCE& aRef,
|
|
const wxString& aFieldName )
|
|
{
|
|
SCH_FIELD* field = aRef.GetSymbol()->GetFieldByName( aFieldName );
|
|
|
|
if( field )
|
|
return field->GetShownText( &aRef.GetSheetPath(), false );
|
|
|
|
// Handle fields with variables as names that are not present in the symbol
|
|
// by giving them the correct value by resolving against the symbol
|
|
if( IsTextVar( aFieldName ) )
|
|
{
|
|
int depth = 0;
|
|
const SCH_SHEET_PATH& path = aRef.GetSheetPath();
|
|
|
|
std::function<bool( wxString* )> symbolResolver =
|
|
[&]( wxString* token ) -> bool
|
|
{
|
|
return aRef.GetSymbol()->ResolveTextVar( &path, token, depth + 1 );
|
|
};
|
|
|
|
return ExpandTextVars( aFieldName, &symbolResolver );
|
|
}
|
|
|
|
return wxEmptyString;
|
|
}
|
|
|
|
|
|
bool FIELDS_EDITOR_GRID_DATA_MODEL::isAttribute( const wxString& aFieldName )
|
|
{
|
|
return aFieldName == wxS( "${DNP}" )
|
|
|| aFieldName == wxS( "${EXCLUDE_FROM_BOARD}" )
|
|
|| aFieldName == wxS( "${EXCLUDE_FROM_BOM}" )
|
|
|| aFieldName == wxS( "${EXCLUDE_FROM_SIM}" );
|
|
}
|
|
|
|
|
|
wxString FIELDS_EDITOR_GRID_DATA_MODEL::getAttributeValue( const SCH_SYMBOL& aSymbol,
|
|
const wxString& aAttributeName )
|
|
{
|
|
if( aAttributeName == wxS( "${DNP}" ) )
|
|
return aSymbol.GetDNP() ? wxS( "1" ) : wxS( "0" );
|
|
|
|
if( aAttributeName == wxS( "${EXCLUDE_FROM_BOARD}" ) )
|
|
return aSymbol.GetExcludedFromBoard() ? wxS( "1" ) : wxS( "0" );
|
|
|
|
if( aAttributeName == wxS( "${EXCLUDE_FROM_BOM}" ) )
|
|
return aSymbol.GetExcludedFromBOM() ? wxS( "1" ) : wxS( "0" );
|
|
|
|
if( aAttributeName == wxS( "${EXCLUDE_FROM_SIM}" ) )
|
|
return aSymbol.GetExcludedFromSim() ? wxS( "1" ) : wxS( "0" );
|
|
|
|
return wxS( "0" );
|
|
}
|
|
|
|
void FIELDS_EDITOR_GRID_DATA_MODEL::setAttributeValue( SCH_SYMBOL& aSymbol,
|
|
const wxString& aAttributeName,
|
|
const wxString& aValue )
|
|
{
|
|
if( aAttributeName == wxS( "${DNP}" ) )
|
|
aSymbol.SetDNP( aValue == wxS( "1" ) );
|
|
else if( aAttributeName == wxS( "${EXCLUDE_FROM_BOARD}" ) )
|
|
aSymbol.SetExcludedFromBoard( aValue == wxS( "1" ) );
|
|
else if( aAttributeName == wxS( "${EXCLUDE_FROM_BOM}" ) )
|
|
aSymbol.SetExcludedFromBOM( aValue == wxS( "1" ) );
|
|
else if( aAttributeName == wxS( "${EXCLUDE_FROM_SIM}" ) )
|
|
aSymbol.SetExcludedFromSim( aValue == wxS( "1" ) );
|
|
}
|
|
|
|
|
|
void FIELDS_EDITOR_GRID_DATA_MODEL::EnableRebuilds()
|
|
{
|
|
m_rebuildsEnabled = true;
|
|
}
|
|
|
|
|
|
void FIELDS_EDITOR_GRID_DATA_MODEL::DisableRebuilds()
|
|
{
|
|
m_rebuildsEnabled = false;
|
|
}
|
|
|
|
|
|
void FIELDS_EDITOR_GRID_DATA_MODEL::RebuildRows()
|
|
{
|
|
if( !m_rebuildsEnabled )
|
|
return;
|
|
|
|
if( GetView() )
|
|
{
|
|
// Commit any pending in-place edits before the row gets moved out from under
|
|
// the editor.
|
|
static_cast<WX_GRID*>( GetView() )->CommitPendingChanges( true );
|
|
|
|
wxGridTableMessage msg( this, wxGRIDTABLE_NOTIFY_ROWS_DELETED, 0, m_rows.size() );
|
|
GetView()->ProcessTableMessage( msg );
|
|
}
|
|
|
|
m_rows.clear();
|
|
|
|
for( unsigned i = 0; i < m_symbolsList.GetCount(); ++i )
|
|
{
|
|
SCH_REFERENCE ref = m_symbolsList[i];
|
|
|
|
if( !m_filter.IsEmpty() && !WildCompareString( m_filter, ref.GetFullRef(), false ) )
|
|
continue;
|
|
|
|
if( m_excludeDNP && ref.GetSymbol()->GetDNP() )
|
|
continue;
|
|
|
|
if( !m_includeExcluded && ref.GetSymbol()->GetExcludedFromBOM() )
|
|
continue;
|
|
|
|
// Check if the symbol if on the current sheet or, in the sheet path somewhere
|
|
// depending on scope
|
|
if( ( m_scope == SCOPE::SCOPE_SHEET && ref.GetSheetPath() != m_path )
|
|
|| ( m_scope == SCOPE::SCOPE_SHEET_RECURSIVE
|
|
&& !ref.GetSheetPath().IsContainedWithin( m_path ) ) )
|
|
{
|
|
continue;
|
|
}
|
|
|
|
bool matchFound = false;
|
|
|
|
// Performance optimization for ungrouped case to skip the N^2 for loop
|
|
if( !m_groupingEnabled && !ref.IsMultiUnit() )
|
|
{
|
|
m_rows.emplace_back( DATA_MODEL_ROW( ref, GROUP_SINGLETON ) );
|
|
continue;
|
|
}
|
|
|
|
// See if we already have a row which this symbol fits into
|
|
for( DATA_MODEL_ROW& row : m_rows )
|
|
{
|
|
// all group members must have identical refs so just use the first one
|
|
SCH_REFERENCE rowRef = row.m_Refs[0];
|
|
|
|
if( unitMatch( ref, rowRef ) )
|
|
{
|
|
matchFound = true;
|
|
row.m_Refs.push_back( ref );
|
|
break;
|
|
}
|
|
else if( m_groupingEnabled && groupMatch( ref, rowRef ) )
|
|
{
|
|
matchFound = true;
|
|
row.m_Refs.push_back( ref );
|
|
row.m_Flag = GROUP_COLLAPSED;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if( !matchFound )
|
|
m_rows.emplace_back( DATA_MODEL_ROW( ref, GROUP_SINGLETON ) );
|
|
}
|
|
|
|
if( GetView() )
|
|
{
|
|
wxGridTableMessage msg( this, wxGRIDTABLE_NOTIFY_ROWS_APPENDED, m_rows.size() );
|
|
GetView()->ProcessTableMessage( msg );
|
|
}
|
|
|
|
Sort();
|
|
}
|
|
|
|
|
|
void FIELDS_EDITOR_GRID_DATA_MODEL::ExpandRow( int aRow )
|
|
{
|
|
std::vector<DATA_MODEL_ROW> children;
|
|
|
|
for( SCH_REFERENCE& ref : m_rows[aRow].m_Refs )
|
|
{
|
|
bool matchFound = false;
|
|
|
|
// See if we already have a child group which this symbol fits into
|
|
for( DATA_MODEL_ROW& child : children )
|
|
{
|
|
// group members are by definition all matching, so just check
|
|
// against the first member
|
|
if( unitMatch( ref, child.m_Refs[0] ) )
|
|
{
|
|
matchFound = true;
|
|
child.m_Refs.push_back( ref );
|
|
break;
|
|
}
|
|
}
|
|
|
|
if( !matchFound )
|
|
children.emplace_back( DATA_MODEL_ROW( ref, CHILD_ITEM ) );
|
|
}
|
|
|
|
if( children.size() < 2 )
|
|
return;
|
|
|
|
std::sort( children.begin(), children.end(),
|
|
[this]( const DATA_MODEL_ROW& lhs, const DATA_MODEL_ROW& rhs ) -> bool
|
|
{
|
|
return cmp( lhs, rhs, this, m_sortColumn, m_sortAscending );
|
|
} );
|
|
|
|
m_rows[aRow].m_Flag = GROUP_EXPANDED;
|
|
m_rows.insert( m_rows.begin() + aRow + 1, children.begin(), children.end() );
|
|
|
|
wxGridTableMessage msg( this, wxGRIDTABLE_NOTIFY_ROWS_INSERTED, aRow, children.size() );
|
|
GetView()->ProcessTableMessage( msg );
|
|
}
|
|
|
|
|
|
void FIELDS_EDITOR_GRID_DATA_MODEL::CollapseRow( int aRow )
|
|
{
|
|
auto firstChild = m_rows.begin() + aRow + 1;
|
|
auto afterLastChild = firstChild;
|
|
int deleted = 0;
|
|
|
|
while( afterLastChild != m_rows.end() && afterLastChild->m_Flag == CHILD_ITEM )
|
|
{
|
|
deleted++;
|
|
afterLastChild++;
|
|
}
|
|
|
|
m_rows[aRow].m_Flag = GROUP_COLLAPSED;
|
|
m_rows.erase( firstChild, afterLastChild );
|
|
|
|
wxGridTableMessage msg( this, wxGRIDTABLE_NOTIFY_ROWS_DELETED, aRow + 1, deleted );
|
|
GetView()->ProcessTableMessage( msg );
|
|
}
|
|
|
|
|
|
void FIELDS_EDITOR_GRID_DATA_MODEL::ExpandCollapseRow( int aRow )
|
|
{
|
|
DATA_MODEL_ROW& group = m_rows[aRow];
|
|
|
|
if( group.m_Flag == GROUP_COLLAPSED )
|
|
ExpandRow( aRow );
|
|
else if( group.m_Flag == GROUP_EXPANDED )
|
|
CollapseRow( aRow );
|
|
}
|
|
|
|
|
|
void FIELDS_EDITOR_GRID_DATA_MODEL::CollapseForSort()
|
|
{
|
|
for( size_t i = 0; i < m_rows.size(); ++i )
|
|
{
|
|
if( m_rows[i].m_Flag == GROUP_EXPANDED )
|
|
{
|
|
CollapseRow( i );
|
|
m_rows[i].m_Flag = GROUP_COLLAPSED_DURING_SORT;
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
void FIELDS_EDITOR_GRID_DATA_MODEL::ExpandAfterSort()
|
|
{
|
|
for( size_t i = 0; i < m_rows.size(); ++i )
|
|
{
|
|
if( m_rows[i].m_Flag == GROUP_COLLAPSED_DURING_SORT )
|
|
ExpandRow( i );
|
|
}
|
|
}
|
|
|
|
|
|
void FIELDS_EDITOR_GRID_DATA_MODEL::ApplyData(
|
|
std::function<void( SCH_SYMBOL&, SCH_SHEET_PATH& )> symbolChangeHandler )
|
|
{
|
|
for( unsigned i = 0; i < m_symbolsList.GetCount(); ++i )
|
|
{
|
|
SCH_SYMBOL& symbol = *m_symbolsList[i].GetSymbol();
|
|
|
|
symbolChangeHandler( symbol, m_symbolsList[i].GetSheetPath() );
|
|
|
|
const std::map<wxString, wxString>& fieldStore = m_dataStore[symbol.m_Uuid];
|
|
|
|
for( const std::pair<wxString, wxString> srcData : fieldStore )
|
|
{
|
|
const wxString& srcName = srcData.first;
|
|
const wxString& srcValue = srcData.second;
|
|
|
|
// Attributes bypass the field logic, so handle them first
|
|
if( isAttribute( srcName ) )
|
|
{
|
|
setAttributeValue( *m_symbolsList[i].GetSymbol(), srcName, srcValue );
|
|
continue;
|
|
}
|
|
|
|
// Skip special fields with variables as names (e.g. ${QUANTITY}),
|
|
// they can't be edited
|
|
if( IsTextVar( srcName ) )
|
|
continue;
|
|
|
|
SCH_FIELD* destField = symbol.FindField( srcName );
|
|
int col = GetFieldNameCol( srcName );
|
|
bool userAdded = ( col != -1 && m_cols[col].m_userAdded );
|
|
|
|
// Add a not existing field if it has a value for this symbol
|
|
bool createField = !destField && ( !srcValue.IsEmpty() || userAdded );
|
|
|
|
if( createField )
|
|
{
|
|
const VECTOR2I symbolPos = symbol.GetPosition();
|
|
destField = symbol.AddField( SCH_FIELD( symbolPos, -1, &symbol, srcName ) );
|
|
}
|
|
|
|
if( !destField )
|
|
continue;
|
|
|
|
if( destField->GetId() == REFERENCE_FIELD )
|
|
{
|
|
// Reference is not editable from this dialog
|
|
}
|
|
else if( destField->GetId() == VALUE_FIELD )
|
|
{
|
|
// Value field cannot be empty
|
|
if( !srcValue.IsEmpty() )
|
|
symbol.SetValueFieldText( srcValue );
|
|
}
|
|
else if( destField->GetId() == FOOTPRINT_FIELD )
|
|
{
|
|
symbol.SetFootprintFieldText( srcValue );
|
|
}
|
|
else
|
|
{
|
|
destField->SetText( srcValue );
|
|
}
|
|
}
|
|
|
|
for( int ii = symbol.GetFields().size() - 1; ii >= MANDATORY_FIELDS; ii-- )
|
|
{
|
|
if( fieldStore.count( symbol.GetFields()[ii].GetName() ) == 0 )
|
|
symbol.GetFields().erase( symbol.GetFields().begin() + ii );
|
|
}
|
|
}
|
|
|
|
m_edited = false;
|
|
}
|
|
|
|
|
|
int FIELDS_EDITOR_GRID_DATA_MODEL::GetDataWidth( int aCol )
|
|
{
|
|
int width = 0;
|
|
|
|
if( ColIsReference( aCol ) )
|
|
{
|
|
for( int row = 0; row < GetNumberRows(); ++row )
|
|
width = std::max( width, KIUI::GetTextSize( GetValue( row, aCol ), GetView() ).x );
|
|
}
|
|
else
|
|
{
|
|
wxString fieldName = GetColFieldName( aCol ); // symbol fieldName or Qty string
|
|
|
|
for( unsigned symbolRef = 0; symbolRef < m_symbolsList.GetCount(); ++symbolRef )
|
|
{
|
|
const KIID& symbolID = m_symbolsList[symbolRef].GetSymbol()->m_Uuid;
|
|
wxString text = m_dataStore[symbolID][fieldName];
|
|
|
|
width = std::max( width, KIUI::GetTextSize( text, GetView() ).x );
|
|
}
|
|
}
|
|
|
|
return width;
|
|
}
|
|
|
|
|
|
void FIELDS_EDITOR_GRID_DATA_MODEL::ApplyBomPreset( const BOM_PRESET& aPreset )
|
|
{
|
|
// Hide and un-group everything by default
|
|
for( size_t i = 0; i < m_cols.size(); i++ )
|
|
{
|
|
SetShowColumn( i, false );
|
|
SetGroupColumn( i, false );
|
|
}
|
|
|
|
std::vector<wxString> order;
|
|
|
|
// Set columns that are present and shown
|
|
for( BOM_FIELD field : aPreset.fieldsOrdered )
|
|
{
|
|
order.emplace_back( field.name );
|
|
|
|
int col = GetFieldNameCol( field.name );
|
|
|
|
// Add any missing fields, if the user doesn't add any data
|
|
// they won't be saved to the symbols anyway
|
|
if( col == -1 )
|
|
{
|
|
AddColumn( field.name, field.label, true );
|
|
col = GetFieldNameCol( field.name );
|
|
}
|
|
else
|
|
SetColLabelValue( col, field.label );
|
|
|
|
SetGroupColumn( col, field.groupBy );
|
|
SetShowColumn( col, field.show );
|
|
}
|
|
|
|
// Set grouping columns
|
|
SetGroupingEnabled( aPreset.groupSymbols );
|
|
|
|
SetFieldsOrder( order );
|
|
|
|
// Set our sorting
|
|
int sortCol = GetFieldNameCol( aPreset.sortField );
|
|
|
|
if( sortCol != -1 )
|
|
SetSorting( sortCol, aPreset.sortAsc );
|
|
else
|
|
SetSorting( GetFieldNameCol( GetCanonicalFieldName( REFERENCE_FIELD ) ), aPreset.sortAsc );
|
|
|
|
SetFilter( aPreset.filterString );
|
|
SetExcludeDNP( aPreset.excludeDNP );
|
|
SetIncludeExcludedFromBOM( aPreset.includeExcludedFromBOM );
|
|
|
|
RebuildRows();
|
|
}
|
|
|
|
|
|
BOM_PRESET FIELDS_EDITOR_GRID_DATA_MODEL::GetBomSettings()
|
|
{
|
|
BOM_PRESET current;
|
|
current.readOnly = false;
|
|
current.fieldsOrdered = GetFieldsOrdered();
|
|
current.sortField = GetColFieldName( GetSortCol() );
|
|
current.sortAsc = GetSortAsc();
|
|
current.filterString = GetFilter();
|
|
current.groupSymbols = GetGroupingEnabled();
|
|
current.excludeDNP = GetExcludeDNP();
|
|
current.includeExcludedFromBOM = GetIncludeExcludedFromBOM();
|
|
|
|
return current;
|
|
}
|
|
|
|
|
|
wxString FIELDS_EDITOR_GRID_DATA_MODEL::Export( const BOM_FMT_PRESET& settings )
|
|
{
|
|
wxString out;
|
|
|
|
if( m_cols.empty() )
|
|
return out;
|
|
|
|
int last_col = -1;
|
|
|
|
// Find the location for the line terminator
|
|
for( size_t col = 0; col < m_cols.size(); col++ )
|
|
{
|
|
if( m_cols[col].m_show )
|
|
last_col = (int) col;
|
|
}
|
|
|
|
// No shown columns
|
|
if( last_col == -1 )
|
|
return out;
|
|
|
|
auto formatField = [&]( wxString field, bool last ) -> wxString
|
|
{
|
|
if( !settings.keepLineBreaks )
|
|
{
|
|
field.Replace( wxS( "\r" ), wxS( "" ) );
|
|
field.Replace( wxS( "\n" ), wxS( "" ) );
|
|
}
|
|
|
|
if( !settings.keepTabs )
|
|
{
|
|
field.Replace( wxS( "\t" ), wxS( "" ) );
|
|
}
|
|
|
|
if( !settings.stringDelimiter.IsEmpty() )
|
|
field.Replace( settings.stringDelimiter,
|
|
settings.stringDelimiter + settings.stringDelimiter );
|
|
|
|
return settings.stringDelimiter + field + settings.stringDelimiter
|
|
+ ( last ? wxString( wxS( "\n" ) ) : settings.fieldDelimiter );
|
|
};
|
|
|
|
// Column names
|
|
for( size_t col = 0; col < m_cols.size(); col++ )
|
|
{
|
|
if( !m_cols[col].m_show )
|
|
continue;
|
|
|
|
out.Append( formatField( m_cols[col].m_label, col == (size_t) last_col ) );
|
|
}
|
|
|
|
// Data rows
|
|
for( size_t row = 0; row < m_rows.size(); row++ )
|
|
{
|
|
// Don't output child rows
|
|
if( GetRowFlags( (int) row ) == CHILD_ITEM )
|
|
continue;
|
|
|
|
for( size_t col = 0; col < m_cols.size(); col++ )
|
|
{
|
|
if( !m_cols[col].m_show )
|
|
continue;
|
|
|
|
// Get the unanottated version of the field, e.g. no "> " or "v " by
|
|
out.Append( formatField( GetExportValue( (int) row, (int) col, settings.refDelimiter,
|
|
settings.refRangeDelimiter ),
|
|
col == (size_t) last_col ) );
|
|
}
|
|
}
|
|
|
|
return out;
|
|
}
|
|
|
|
|
|
void FIELDS_EDITOR_GRID_DATA_MODEL::AddReferences( const SCH_REFERENCE_LIST& aRefs )
|
|
{
|
|
for( const SCH_REFERENCE& ref : aRefs )
|
|
{
|
|
if( !m_symbolsList.Contains( ref ) )
|
|
{
|
|
m_symbolsList.AddItem( ref );
|
|
|
|
// Update the fields of every reference
|
|
for( const SCH_FIELD& field : ref.GetSymbol()->GetFields() )
|
|
m_dataStore[ref.GetSymbol()->m_Uuid][field.GetCanonicalName()] = field.GetText();
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
void FIELDS_EDITOR_GRID_DATA_MODEL::RemoveSymbol( const SCH_SYMBOL& aSymbol )
|
|
{
|
|
// The schematic event listener passes us the symbol after it has been removed,
|
|
// so we can't just work with a SCH_REFERENCE_LIST like the other handlers as the
|
|
// references are already gone. Instead we need to prune our list.
|
|
m_dataStore[aSymbol.m_Uuid].clear();
|
|
|
|
// Remove all refs that match this symbol using remove_if
|
|
m_symbolsList.erase( std::remove_if( m_symbolsList.begin(), m_symbolsList.end(),
|
|
[&aSymbol]( const SCH_REFERENCE& ref ) -> bool
|
|
{
|
|
return ref.GetSymbol()->m_Uuid == aSymbol.m_Uuid;
|
|
} ),
|
|
m_symbolsList.end() );
|
|
}
|
|
|
|
|
|
void FIELDS_EDITOR_GRID_DATA_MODEL::RemoveReferences( const SCH_REFERENCE_LIST& aRefs )
|
|
{
|
|
for( const SCH_REFERENCE& ref : aRefs )
|
|
{
|
|
int index = m_symbolsList.FindRefByFullPath( ref.GetFullPath() );
|
|
|
|
if( index != -1 )
|
|
{
|
|
m_symbolsList.RemoveItem( index );
|
|
|
|
// If we're out of instances then remove the symbol, too
|
|
if( ref.GetSymbol()->GetInstances().empty() )
|
|
m_dataStore.erase( ref.GetSymbol()->m_Uuid );
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
void FIELDS_EDITOR_GRID_DATA_MODEL::UpdateReferences( const SCH_REFERENCE_LIST& aRefs )
|
|
{
|
|
for( const SCH_REFERENCE& ref : aRefs )
|
|
{
|
|
// Update the fields of every reference. Do this by iterating through the data model
|
|
// columns; we must have all fields in the symbol added to the data model at this point,
|
|
// and some of the data model columns may be variables that are not present in the symbol
|
|
for( const DATA_MODEL_COL& col : m_cols )
|
|
updateDataStoreSymbolField( *ref.GetSymbol(), col.m_fieldName );
|
|
|
|
if( !m_symbolsList.Contains( ref ) )
|
|
m_symbolsList.AddItem( ref );
|
|
}
|
|
}
|