kicad/eeschema/bom_table_model.cpp

1546 lines
37 KiB
C++

/*
* This program source code file is part of KiCad, a free EDA CAD application.
*
* Copyright (C) 2017 Oliver Walters
* Copyright (C) 2017 KiCad Developers, see CHANGELOG.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 "bom_table_model.h"
// Indicator that multiple values exist in child rows
#define ROW_MULT_ITEMS wxString( "<...>" )
static const wxColor ROW_COLOUR_ITEM_CHANGED( 200, 0, 0 );
static const wxColor ROW_COLOUR_MULTIPLE_ITEMS( 60, 90, 200 );
/**
* Convert BOM_TABLE_ROW -> wxDataViewItem
*/
static wxDataViewItem RowToItem( BOM_TABLE_ROW const* aRow )
{
return wxDataViewItem( const_cast<void*>( static_cast<void const*>( aRow ) ) );
}
/**
* Convert wxDataViewItem -> BOM_TABEL_ROW
*/
static BOM_TABLE_ROW const* ItemToRow( wxDataViewItem aItem )
{
if( !aItem.IsOk() )
{
return nullptr;
}
else
{
return static_cast<BOM_TABLE_ROW const*>( aItem.GetID() );
}
}
BOM_FIELD_VALUES::BOM_FIELD_VALUES( wxString aRefDes, FIELD_VALUE_MAP* aTemplate ) :
m_refDes( aRefDes ),
m_templateValues( aTemplate )
{
}
/**
* Return the current value for the provided field ID
*/
bool BOM_FIELD_VALUES::GetFieldValue( unsigned int aFieldId, wxString& aValue ) const
{
auto search = m_currentValues.find( aFieldId );
if( search == m_currentValues.end() )
return false;
aValue = search->second;
return true;
}
/**
* Return the backup value for the provided field ID
*/
bool BOM_FIELD_VALUES::GetBackupValue( unsigned int aFieldId, wxString& aValue ) const
{
auto search = m_backupValues.find( aFieldId );
if( search == m_backupValues.end() )
return false;
aValue = search->second;
return true;
}
/**
* Return the template value for a provided field ID (if it exists)
*/
bool BOM_FIELD_VALUES::GetTemplateValue( unsigned int aFieldId, wxString& aValue ) const
{
if( !m_templateValues )
return false;
auto search = m_templateValues->find( aFieldId );
if( search == m_templateValues->end() )
return false;
aValue = search->second;
return true;
}
/**
* Set the value for the provided field ID
* Field value is set under any of the following conditions:
* - param aOverwrite is true
* - There is no current value
* - The current value is empty
*/
void BOM_FIELD_VALUES::SetFieldValue( unsigned int aFieldId, wxString aValue, bool aOverwrite )
{
if( aOverwrite || m_currentValues.count( aFieldId ) == 0 || m_currentValues[aFieldId].IsEmpty() )
{
m_currentValues[aFieldId] = aValue;
}
}
bool BOM_FIELD_VALUES::HasValueChanged( unsigned int aFieldId) const
{
wxString currentValue, backupValue;
GetFieldValue( aFieldId, currentValue );
GetBackupValue( aFieldId, backupValue );
return currentValue.Cmp( backupValue ) != 0;
}
void BOM_FIELD_VALUES::RevertChanges( unsigned int aFieldId )
{
wxString backupValue;
GetBackupValue( aFieldId, backupValue );
SetFieldValue( aFieldId, backupValue, true );
}
void BOM_FIELD_VALUES::SetBackupPoint()
{
for( auto it = m_currentValues.begin(); it != m_currentValues.end(); ++it )
{
m_backupValues[it->first] = it->second;
}
}
BOM_TABLE_ROW::BOM_TABLE_ROW() : m_columnList( nullptr )
{
}
/**
* Update cell attributes based on parameters of the cell
* Default implementation highlights cells that have been altered
*/
bool BOM_TABLE_ROW::GetAttr( unsigned int aFieldId, wxDataViewItemAttr& aAttr ) const
{
auto field = m_columnList->GetColumnById( aFieldId );
if( HasValueChanged( field ) )
{
aAttr.SetBold( true );
aAttr.SetItalic( true );
aAttr.SetColour( ROW_COLOUR_ITEM_CHANGED );
return true;
}
return false;
}
bool BOM_TABLE_ROW::HasChanged() const
{
if( !m_columnList )
return false;
for( auto& column : m_columnList->Columns )
{
if( column && HasValueChanged( column ) )
{
return true;
}
}
return false;
}
/**
* Create a new group (which contains one or more components)
*/
BOM_TABLE_GROUP::BOM_TABLE_GROUP( BOM_COLUMN_LIST* aColumnList )
{
m_columnList = aColumnList;
}
bool BOM_TABLE_GROUP::GetAttr( unsigned int aFieldId, wxDataViewItemAttr& aAttr ) const
{
if( GetFieldValue( aFieldId ).Cmp( ROW_MULT_ITEMS ) == 0 )
{
aAttr.SetItalic( true );
aAttr.SetColour( ROW_COLOUR_MULTIPLE_ITEMS );
return true;
}
return BOM_TABLE_ROW::GetAttr( aFieldId, aAttr );
}
/**
* Return the value associated with a given field in the group.
* Some fields require special attention.
*/
wxString BOM_TABLE_GROUP::GetFieldValue( unsigned int aFieldId ) const
{
wxString value;
// Account for special cases
switch( aFieldId )
{
// QUANTITY returns the size of the group
case BOM_COL_ID_QUANTITY:
value = wxString::Format( "%u", (unsigned int) GroupSize() );
break;
// REFERENCE field returns consolidated list of references
case BOM_COL_ID_REFERENCE:
value = wxJoin( GetReferences(), ' ' );
break;
// Otherwise, return component data
default:
if( Components.size() == 0 )
{
value = wxEmptyString;
}
else
{
// If the components in this group contain multiple items,
// display a special string indicating this
for( unsigned int i=0; i<Components.size(); i++ )
{
auto const& cmp = Components[i];
if( i == 0 )
{
value = cmp->GetFieldValue( aFieldId );
}
// Mismatch found
else if( value.Cmp( cmp->GetFieldValue( aFieldId ) ) != 0 )
{
value = ROW_MULT_ITEMS;
break;
}
}
}
break;
}
return value;
}
/**
* Set the value of a field in a group
* The new value is pushed to all components that are children of this group
*/
bool BOM_TABLE_GROUP::SetFieldValue( unsigned int aFieldId, const wxString aValue, bool aOverwrite )
{
bool result = false;
for( auto& cmp : Components )
{
result |= cmp->SetFieldValue( aFieldId, aValue, aOverwrite );
}
return result;
}
/**
* Determines if a given component matches against a particular field.
*
* - Tests each field in turn; all fields must match
* - Some fields require special checking
*
* @aFieldId - The ID of the field
* @aCmp - The component being tested
*/
bool BOM_TABLE_GROUP::TestField( BOM_COLUMN* aField, BOM_TABLE_COMPONENT* aComponent ) const
{
if( !aField || !aComponent )
return false;
if( Components.size() == 0 )
return true;
wxString componentValue;
wxString comparisonValue;
// Some fields are handled in a special manner
// (handle these first)
switch( aField->Id() )
{
// These fields should NOT be compared (return True)
case BOM_COL_ID_QUANTITY:
return true;
// Reference matching is done only on prefix
case BOM_COL_ID_REFERENCE:
componentValue = aComponent->GetPrefix();
comparisonValue = Components[0]->GetPrefix();
break;
default:
componentValue = aComponent->GetFieldValue( aField->Id() );
comparisonValue = Components[0]->GetFieldValue( aField->Id() );
break;
}
bool result = componentValue.Cmp( comparisonValue ) == 0;
return result;
}
/**
* Add a new component to the group.
* It is assumed at this stage that the component is a good match for the group.
* @param aCmp is the new component to add
*/
bool BOM_TABLE_GROUP::AddComponent( BOM_TABLE_COMPONENT* aComponent )
{
if( !aComponent )
return false;
// To be a match, all fields must match!
bool match = true;
for( auto* column : m_columnList->Columns )
{
// Ignore any columns marked as "not used for sorting"
if( !column->IsUsedToSort() )
continue;
match = TestField( column, aComponent );
// Escape on first mismatch
if( !match )
{
break;
}
}
if( match )
{
aComponent->SetParent( this );
Components.push_back( aComponent );
return true;
}
else
{
return false;
}
return false;
}
/**
* Adds each child row to the supplied list, and returns the total child count
*/
unsigned int BOM_TABLE_GROUP::GetChildren( wxDataViewItemArray& aChildren ) const
{
// Show drop-down for child components
for( auto& row : Components )
{
if( row )
{
aChildren.push_back( RowToItem( &*row ) );
}
}
return aChildren.size();
}
/**
* Test if any components in this group have a new value in the provided field
* @param aField is the field to test
* @return true if any children have changed else false
*/
bool BOM_TABLE_GROUP::HasValueChanged( BOM_COLUMN* aField ) const
{
bool changed = false;
if( !aField )
return false;
for( auto const& row : Components )
{
if( row->HasValueChanged( aField ) )
{
changed = true;
break;
}
}
// If the value has changed, the group field data must be updated
if( changed )
{
//TODO
}
return changed;
}
/**
* Return a list of (ordered) references
* for all the components in this group
*
* @aSort - Sort the references
*/
wxArrayString BOM_TABLE_GROUP::GetReferences( bool aSort ) const
{
wxArrayString refs;
for( auto const& cmp : Components )
{
if( cmp )
{
refs.Add( cmp->GetFieldValue( BOM_COL_ID_REFERENCE ) );
}
}
if( aSort )
{
refs.Sort( SortReferences );
}
return refs;
}
/**
* Compare two references (e.g. "R100", "R19") and perform a 'natural' sort
* This sorting must preserve numerical order rather than alphabetical
* e.g. "R100" is lower (alphabetically) than "R19"
* BUT should be placed after R19
*/
int BOM_TABLE_GROUP::SortReferences( const wxString& aFirst, const wxString& aSecond )
{
// Default sorting
int defaultSort = aFirst.Cmp( aSecond );
static const wxString REGEX_STRING = "^([a-zA-Z]+)(\\d+)$";
// Compile regex statically
static wxRegEx regexFirst( REGEX_STRING, wxRE_ICASE | wxRE_ADVANCED );
static wxRegEx regexSecond( REGEX_STRING, wxRE_ICASE | wxRE_ADVANCED );
if( !regexFirst.Matches( aFirst ) || !regexSecond.Matches( aSecond ) )
{
return defaultSort;
}
// First priority is to order by prefix
wxString prefixFirst = regexFirst.GetMatch( aFirst, 1 );
wxString prefixSecond = regexSecond.GetMatch( aSecond, 1 );
if( prefixFirst.CmpNoCase( prefixSecond ) != 0 ) // Different prefixes!
{
return defaultSort;
}
wxString numStrFirst = regexFirst.GetMatch( aFirst, 2 );
wxString numStrSecond = regexSecond.GetMatch( aSecond, 2 );
// If either match failed, just return normal string comparison
if( numStrFirst.IsEmpty() || numStrSecond.IsEmpty() )
{
return defaultSort;
}
// Convert each number string to an integer
long numFirst = 0;
long numSecond = 0;
// If either conversion fails, return normal string comparison
if( !numStrFirst.ToLong( &numFirst ) || !numStrSecond.ToLong( &numSecond ) )
{
return defaultSort;
}
return (int) (numFirst - numSecond);
}
/**
* Compare two VALUE fields.
* A value field can reasonably be expected to be one of:
* a) Purely numerical e.g. '22'
* b) Numerical with included units e.g. '15uF'
* c) Numerical with included prefix but no units e.g. '20n'
* d) Numerical with prefix inside number e.g. '4K7'
* e) Other, e.g. 'MAX232'
*
* Cases a) to d) should be detected and converted to a common representation
* Values that do not match this pattern should revert to standard string comparison
*/
int BOM_TABLE_GROUP::SortValues( const wxString& aFirst, const wxString& aSecond )
{
//TODO - Intelligent comparison of component values
// e.g. 4K > 499
// e.g. 1nF < 0.1u
// For now, just return default comparison
return aFirst.CmpNoCase( aSecond );
}
/**
* Create a new COMPONENT row
* Each COMPONENT row is associated with a single component item.
*/
BOM_TABLE_COMPONENT::BOM_TABLE_COMPONENT( BOM_TABLE_GROUP* aParent,
BOM_COLUMN_LIST* aColumnList,
BOM_FIELD_VALUES* aFieldValues )
{
m_parent = aParent;
m_columnList = aColumnList;
m_fieldValues = aFieldValues;
}
/**
* Try to add a unit to this component
* If the references match, it will be added
*/
bool BOM_TABLE_COMPONENT::AddUnit( SCH_REFERENCE aUnit )
{
// Addition is successful if the references match or there are currently no units in the group
if( Units.size() == 0 || Units[0].GetRef().Cmp( aUnit.GetRef() ) == 0 )
{
Units.push_back( aUnit );
wxString value;
// Extract the component data
for( auto column : m_columnList->Columns )
{
auto cmp = aUnit.GetComp();
switch( column->Id() )
{
case BOM_COL_ID_QUANTITY:
value = wxEmptyString;
break;
case BOM_COL_ID_DESCRIPTION:
value = cmp->GetAliasDescription();
break;
case BOM_COL_ID_DATASHEET:
value = cmp->GetField( DATASHEET )->GetText();
if( value.IsEmpty() )
{
value = cmp->GetAliasDocumentation();
}
break;
case BOM_COL_ID_REFERENCE:
value = aUnit.GetRef();
break;
case BOM_COL_ID_VALUE:
value = cmp->GetField( VALUE )->GetText();
break;
case BOM_COL_ID_FOOTPRINT:
value = cmp->GetField( FOOTPRINT )->GetText();
break;
// User fields
default:
value = cmp->GetFieldText( column->Title() );
break;
}
m_fieldValues->SetFieldValue( column->Id(), value );
}
return true;
}
return false;
}
/**
* Return the value associated with a particular field
* If no field is found, return an empty string
*/
wxString BOM_TABLE_COMPONENT::GetFieldValue( unsigned int aFieldId ) const
{
wxString value;
switch ( aFieldId )
{
case BOM_COL_ID_QUANTITY:
return wxEmptyString;
case BOM_COL_ID_REFERENCE:
return GetReference();
default:
break;
}
if( m_fieldValues )
{
m_fieldValues->GetFieldValue( aFieldId, value );
if( value.IsEmpty() )
{
m_fieldValues->GetTemplateValue( aFieldId, value );
}
}
return value;
}
/**
* Set the value of a field in the component
* @param aFieldId is the unique ID of the field to update
* @param aValue is the new value
* @param aOverwrite enforces writing even if a value exists
*/
bool BOM_TABLE_COMPONENT::SetFieldValue( unsigned int aFieldId, const wxString aValue, bool aOverwrite )
{
if( m_fieldValues )
{
m_fieldValues->SetFieldValue( aFieldId, aValue, aOverwrite );
return true;
}
return false;
}
/**
* Return the prefix of a component e.g. "R23" -> "R"
*/
wxString BOM_TABLE_COMPONENT::GetPrefix() const
{
if( Units.size() == 0 )
return wxEmptyString;
return Units[0].GetComp()->GetPrefix();
}
/**
* Return the reference of a component e.g. "R23"
*/
wxString BOM_TABLE_COMPONENT::GetReference() const
{
if( Units.size() == 0 )
return wxEmptyString;
return Units[0].GetRef();
}
/**
* Determines if the given field has been changed for this component
*/
bool BOM_TABLE_COMPONENT::HasValueChanged( BOM_COLUMN* aField ) const
{
if( !aField )
{
return false;
}
return m_fieldValues->HasValueChanged( aField->Id() );
}
/**
* If any changes have been made to this component,
* they are now applied to the schematic component
*/
void BOM_TABLE_COMPONENT::ApplyFieldChanges()
{
for( auto& unit : Units )
{
auto cmp = unit.GetComp();
if( !cmp )
continue;
// Iterate over each column
SCH_FIELD* field;
for( auto& column : m_columnList->Columns )
{
if( column && HasValueChanged( column ) )
{
wxString value = GetFieldValue( column->Id() );
switch( column->Id() )
{
// Ignore read-only fields
case BOM_COL_ID_REFERENCE:
case BOM_COL_ID_QUANTITY:
continue;
// Special field considerations
case BOM_COL_ID_FOOTPRINT:
field = cmp->GetField( FOOTPRINT );
break;
case BOM_COL_ID_VALUE:
field = cmp->GetField( VALUE );
break;
case BOM_COL_ID_DATASHEET:
field = cmp->GetField( DATASHEET );
break;
default:
field = cmp->FindField( column->Title() );
break;
}
// New field needs to be added?
if( !field && !value.IsEmpty() )
{
SCH_FIELD newField( wxPoint( 0, 0 ), -1, cmp, column->Title() );
field = cmp->AddField( newField );
}
if( field )
{
field->SetText( value );
}
}
}
}
}
/**
* Revert the displayed fields for this component
* to their original values (matching the schematic data)
*/
void BOM_TABLE_COMPONENT::RevertFieldChanges()
{
for( auto& column : m_columnList->Columns )
{
switch( column->Id() )
{
case BOM_COL_ID_REFERENCE:
case BOM_COL_ID_QUANTITY:
continue;
default:
break;
}
m_fieldValues->RevertChanges( column->Id() );
}
}
BOM_TABLE_MODEL::BOM_TABLE_MODEL() :
m_widget( nullptr ),
m_sortingColumn( BOM_COL_ID_REFERENCE ),
m_sortingOrder( true )
{
//TODO
}
/**
* Create a container for the BOM_TABLE_MODEL
* This is required for reference counting by wxDataViewCtrl
*/
BOM_TABLE_MODEL::MODEL_PTR BOM_TABLE_MODEL::Create()
{
auto model = new BOM_TABLE_MODEL();
auto container = BOM_TABLE_MODEL::MODEL_PTR( model );
return container;
}
BOM_TABLE_MODEL::~BOM_TABLE_MODEL()
{
//TODO
}
wxDataViewColumn* BOM_TABLE_MODEL::AddColumn( BOM_COLUMN* aColumn, int aPosition )
{
static const unsigned int flags = wxDATAVIEW_COL_RESIZABLE | wxDATAVIEW_COL_SORTABLE;
if( !m_widget || !aColumn || !aColumn->IsVisible() )
return nullptr;
wxDataViewCellMode editFlag = aColumn->IsReadOnly() ? wxDATAVIEW_CELL_INERT : wxDATAVIEW_CELL_EDITABLE;
auto renderer = new wxDataViewTextRenderer( "string" , editFlag );
auto column = new wxDataViewColumn( aColumn->Title(),
renderer,
aColumn->Id(),
150, //TODO - variable default width?
wxAlignment( wxALIGN_CENTER ),
flags );
// Work out where to insert the column
std::set<unsigned int> columnsBefore;
for( auto testCol : ColumnList.Columns )
{
if( testCol->Id() == aColumn->Id() )
{
break;
}
else
{
columnsBefore.insert( testCol->Id() );
}
}
bool found = false;
for( unsigned int ii=0; ii<m_widget->GetColumnCount(); ii++ )
{
auto col = m_widget->GetColumn( ii );
if( !col )
continue;
// If the new column is already in the view, escape
if( col->GetModelColumn() == aColumn->Id() )
{
return col;
}
// If we should insert the new column BEFORE this one
if( columnsBefore.count( col->GetModelColumn() ) == 0 )
{
found = true;
m_widget->InsertColumn( ii, column );
break;
}
}
if( !found )
m_widget->AppendColumn( column );
column->SetResizeable( true );
return column;
}
/**
* Gracefully remove the given column from the wxDataViewCtrl
* Removing columns individually prevents bad redraw of entire table
*/
bool BOM_TABLE_MODEL::RemoveColumn( BOM_COLUMN* aColumn )
{
if( !m_widget || !aColumn )
return false;
for( unsigned int ii=0; ii<m_widget->GetColumnCount(); ii++ )
{
auto col = m_widget->GetColumn( ii );
if( col && col->GetModelColumn() == aColumn->Id() )
{
m_widget->DeleteColumn( col );
return true;
}
}
return false;
}
/**
* Attach the MODEL to a particular VIEW
* This function causes the view to be updated appropriately
*/
void BOM_TABLE_MODEL::AttachTo( wxDataViewCtrl* aView )
{
if( !aView )
{
return;
}
m_widget = aView;
aView->Freeze();
Cleared();
aView->AssociateModel( this );
aView->ClearColumns();
// Add all columns
for( auto col : ColumnList.Columns )
{
AddColumn( col );
}
aView->Thaw();
// Notify the view that the data needs to be redrawn
aView->Update();
}
/**
* Return the total number of components displayed by the model
*/
unsigned int BOM_TABLE_MODEL::ComponentCount() const
{
unsigned int count = 0;
for( auto& group : Groups )
{
if( group )
count += group->GroupSize();
}
return count;
}
void BOM_TABLE_MODEL::ClearColumns()
{
ColumnList.Clear();
}
/**
* Add default columns to the table
* These columns are ALWAYS available in the table
* They are immutable - can be hidden by user but not removed
*/
void BOM_TABLE_MODEL::AddDefaultColumns()
{
// Reference column is read-only
ColumnList.AddColumn( new BOM_COLUMN(
BOM_COL_ID_REFERENCE,
BOM_COL_TYPE_GENERATED,
BOM_COL_TITLE_REFERENCE,
true, true ) );
ColumnList.AddColumn( new BOM_COLUMN(
BOM_COL_ID_VALUE,
BOM_COL_TYPE_KICAD,
BOM_COL_TITLE_VALUE,
true, false ) );
ColumnList.AddColumn( new BOM_COLUMN(
BOM_COL_ID_FOOTPRINT,
BOM_COL_TYPE_KICAD,
BOM_COL_TITLE_FOOTPRINT,
true, false ) );
ColumnList.AddColumn( new BOM_COLUMN(
BOM_COL_ID_DATASHEET,
BOM_COL_TYPE_KICAD,
BOM_COL_TITLE_DATASHEET,
true, false ) );
// Description comes from .dcm file and is read-only
ColumnList.AddColumn( new BOM_COLUMN(
BOM_COL_ID_DESCRIPTION,
BOM_COL_TYPE_LIBRARY,
BOM_COL_TITLE_DESCRIPTION,
true, true ) );
// Quantity column is read-only
ColumnList.AddColumn( new BOM_COLUMN(
BOM_COL_ID_QUANTITY,
BOM_COL_TYPE_GENERATED,
BOM_COL_TITLE_QUANTITY,
true, true ) );
}
/**
* Extract field data from all components
* Compiles an inclusive list of all field names from all components
*/
void BOM_TABLE_MODEL::AddComponentFields( SCH_COMPONENT* aCmp )
{
std::vector< SCH_FIELD* > fields;
SCH_FIELD* field;
wxString fieldName;
if( nullptr == aCmp )
return;
// Extract custom columns from component
fields.clear();
aCmp->GetFields( fields, false );
// Iterate over custom field datas
for( unsigned int i=MANDATORY_FIELDS; i<fields.size(); i++ )
{
field = fields[i];
if( nullptr == field ) continue;
fieldName = field->GetName();
auto existing = ColumnList.GetColumnByTitle( fieldName );
// As columns are sorted by ID, we can allow user to
// create a column with a "special" name
if( existing && existing->Id() >= MANDATORY_FIELDS)
continue;
ColumnList.AddColumn( new BOM_COLUMN( ColumnList.NextFieldId(),
BOM_COL_TYPE_USER,
field->GetName(),
true, false ) );
}
}
/**
* Add a list of component items to the BOM manager
* Creates consolidated groups of components as required
*/
void BOM_TABLE_MODEL::SetComponents( SCH_REFERENCE_LIST aRefs, const TEMPLATE_FIELDNAMES& aTemplateFields )
{
// Add default columns
AddDefaultColumns();
// Extract all component fields
for( unsigned int ii=0; ii<aRefs.GetCount(); ii++ )
{
auto ref = aRefs.GetItem( ii );
auto cmp = ref.GetComp();
if( cmp )
{
AddComponentFields( cmp );
}
}
// Add template fields if they are not already added
for( auto field : aTemplateFields )
{
BOM_COLUMN* col;
col = ColumnList.GetColumnByTitle( field.m_Name );
if( !col )
{
col = new BOM_COLUMN( ColumnList.NextFieldId(),
BOM_COL_TYPE_USER,
field.m_Name,
true, false );
ColumnList.AddColumn( col );
}
// Add template value for that field
m_fieldTemplates[col->Id()] = field.m_Value;
}
// Group multi-unit components together
m_components.clear();
m_fieldValues.clear();
// Iterate through each unique component
for( unsigned int ii=0; ii<aRefs.GetCount(); ii++ )
{
auto ref = aRefs.GetItem( ii );
bool found = false;
for( auto& cmp : m_components )
{
if( cmp->AddUnit( ref ) )
{
found = true;
break;
}
}
if( !found )
{
// Find the field:value map associated with this component
wxString refDes = ref.GetComp()->GetField( REFERENCE )->GetText();
bool dataFound = false;
BOM_FIELD_VALUES* values;
for( auto& data : m_fieldValues )
{
// Look for a match based on RefDes
if( data->GetReference().Cmp( refDes ) == 0 )
{
dataFound = true;
values = &*data;
}
}
if( !dataFound )
{
values = new BOM_FIELD_VALUES( refDes, &m_fieldTemplates );
m_fieldValues.push_back( std::unique_ptr<BOM_FIELD_VALUES>( values ) );
}
auto* newComponent = new BOM_TABLE_COMPONENT( nullptr, &ColumnList, values );
newComponent->AddUnit( ref );
m_components.push_back( std::unique_ptr<BOM_TABLE_COMPONENT>( newComponent ) );
}
}
SetBackupPoint();
}
void BOM_TABLE_MODEL::SetBackupPoint()
{
// Mark backup locations for all values
for( auto& vals : m_fieldValues )
{
vals->SetBackupPoint();
}
}
/**
* Recalculate grouping of components and reload table
**/
void BOM_TABLE_MODEL::ReloadTable()
{
if( m_widget )
{
m_widget->Freeze();
}
// Alert the view that the model data has changed
Cleared();
Groups.clear();
for( auto& cmp : m_components )
{
bool grouped = false;
if( m_groupColumns )
{
for( auto& group : Groups )
{
if( group->AddComponent( &*cmp ) )
{
grouped = true;
break;
}
}
}
// No suitable group was found for this component
if( !grouped )
{
auto* newGroup = new BOM_TABLE_GROUP( &ColumnList );
newGroup->AddComponent( &*cmp );
Groups.push_back( std::unique_ptr<BOM_TABLE_GROUP>( newGroup ) );
}
}
// Update the display
if( m_widget )
{
//Cleared();
m_widget->AssociateModel( this );
m_widget->Thaw();
}
}
/**
* Return a string array of data from a given row
*/
wxArrayString BOM_TABLE_MODEL::GetRowData( unsigned int aRow, std::vector<BOM_COLUMN*> aColumns ) const
{
wxArrayString row;
wxString data;
if( Groups.size() <= aRow )
return row;
auto const& group = Groups[aRow];
if ( !group )
return row;
for( auto const col : aColumns )
{
if( !col )
{
row.Add( wxEmptyString );
continue;
}
row.Add( group->GetFieldValue( col->Id() ) );
}
return row;
}
/**
* Get the value of a particular item in the model
*/
void BOM_TABLE_MODEL::GetValue(
wxVariant& aVariant,
const wxDataViewItem& aItem,
unsigned int aFieldId ) const
{
auto row = ItemToRow( aItem );
if( row )
{
aVariant = row->GetFieldValue( aFieldId );
}
}
/**
* Set the value of a particular item in the model
*/
bool BOM_TABLE_MODEL::SetValue(
const wxVariant& aVariant,
const wxDataViewItem& aItem,
unsigned int aFieldId )
{
if( !aItem.IsOk() || !m_widget )
{
return false;
}
// Extract the value to be set
if( aVariant.GetType().Cmp( "string" ) == 0 )
{
wxString value = aVariant.GetString();
bool result = false;
wxDataViewItemArray selectedItems;
m_widget->GetSelections( selectedItems );
// Set the row value for all selected rows
for( auto item : selectedItems )
{
auto selectedRow = static_cast<BOM_TABLE_ROW*>( item.GetID() );
if( selectedRow )
{
result |= selectedRow->SetFieldValue( aFieldId, value, true );
}
}
if( m_widget )
{
m_widget->Update();
}
return result;
}
// Default
return false;
}
/**
* Return the parent item for a given item in the model.
* If no parent is found (or the item is invalid) return an invalid item.
*/
wxDataViewItem BOM_TABLE_MODEL::GetParent( const wxDataViewItem& aItem ) const
{
auto row = ItemToRow( aItem );
auto parent = row ? row->GetParent() : nullptr;
if( parent )
{
return RowToItem( parent );
}
// Return an invalid item
return wxDataViewItem();
}
/**
* Returns true if the supplied item has children
*/
bool BOM_TABLE_MODEL::IsContainer( const wxDataViewItem& aItem ) const
{
auto row = ItemToRow( aItem );
if( row )
{
return row->HasChildren();
}
return true;
}
/**
* Push all children of the supplied item into the list
* If the supplied item is invalid, push all the top-level items
*/
unsigned int BOM_TABLE_MODEL::GetChildren(
const wxDataViewItem& aItem,
wxDataViewItemArray& aChildren ) const
{
auto row = aItem.IsOk() ? ItemToRow( aItem ) : nullptr;
// Valid row, return its children
if( row )
{
return row->GetChildren( aChildren );
}
else
{
for( auto& group : Groups )
{
aChildren.Add( RowToItem( &*group ) );
}
return aChildren.size();
}
}
bool BOM_TABLE_MODEL::GetAttr( const wxDataViewItem& aItem,
unsigned int aFieldId,
wxDataViewItemAttr& aAttr ) const
{
auto row = aItem.IsOk() ? ItemToRow( aItem ) : nullptr;
if( row )
{
return row->GetAttr( aFieldId, aAttr );
}
else
{
return false;
}
}
/**
* Custom comparison function for improved column sorting
* Alphanumeric sorting is not sufficient for correct ordering of some fields
* Some columns are sorted numerically, others with more complex rules.
*/
int BOM_TABLE_MODEL::Compare( const wxDataViewItem& aItem1,
const wxDataViewItem& aItem2,
unsigned int aColumnId,
bool aAscending ) const
{
if( !aItem1.IsOk() || !aItem2.IsOk() )
return 0;
int result = 0;
auto row1 = ItemToRow( aItem1 );
auto row2 = ItemToRow( aItem2 );
if( !row1 || !row2 )
return 0;
if( row1->GetParent() != row2->GetParent() )
return 0;
wxString strVal1 = row1->GetFieldValue( aColumnId );
wxString strVal2 = row2->GetFieldValue( aColumnId );
long numVal1;
long numVal2;
switch( aColumnId )
{
// Reference column sorted by reference val
case BOM_COL_ID_REFERENCE:
result = BOM_TABLE_GROUP::SortReferences( strVal1, strVal2 );
break;
case BOM_COL_ID_VALUE:
result = BOM_TABLE_GROUP::SortValues( strVal1, strVal2 );
break;
// These columns are sorted numerically
case BOM_COL_ID_QUANTITY:
if( strVal1.ToLong( &numVal1 ) && strVal2.ToLong( &numVal2 ) )
{
result = numVal1 - numVal2;
}
else
{
result = strVal1.Cmp( strVal2 );
}
break;
default:
// Default comparison (no special case)
result = strVal1.Cmp( strVal2 );
break;
}
// If initial sorting failed, sort secondly by reference
if( result == 0 && aColumnId != BOM_COL_ID_REFERENCE )
{
result = BOM_TABLE_GROUP::SortReferences(
row1->GetFieldValue( BOM_COL_ID_REFERENCE ),
row2->GetFieldValue( BOM_COL_ID_REFERENCE ) );
}
// If sorting still failed, sort thirdly by value
if( result == 0 && aColumnId != BOM_COL_ID_VALUE )
{
result = BOM_TABLE_GROUP::SortValues(
row1->GetFieldValue( BOM_COL_ID_VALUE ),
row2->GetFieldValue( BOM_COL_ID_VALUE ) );
}
if( !aAscending )
{
result *= -1;
}
return result;
}
/**
* Revert all component data back to the original values.
* The table view is updated accordingly
*/
void BOM_TABLE_MODEL::RevertFieldChanges()
{
for( auto& group : Groups )
{
if( !group )
continue;
bool changed = false;
for( auto& component : group->Components )
{
if( !component )
continue;
if( component->HasChanged() )
{
component->RevertFieldChanges();
ItemChanged( RowToItem( &*component ) );
changed = true;
}
}
// Update the group if any components changed
if( changed )
{
ItemChanged( RowToItem( &*group ) );
}
}
}
/**
* Apply all outstanding field changes.
* This is performed only when the window is closed
*/
void BOM_TABLE_MODEL::ApplyFieldChanges()
{
for( auto& group : Groups )
{
if( !group )
continue;
for( auto& component : group->Components )
{
if( !component )
continue;
if( component->HasChanged() )
{
component->ApplyFieldChanges();
}
}
}
}
/**
* Tests if any component values in the table have been altered
*/
bool BOM_TABLE_MODEL::HaveFieldsChanged() const
{
for( auto const& group : Groups )
{
if( !group )
continue;
for( auto const& cmp : group->Components )
{
if( !cmp )
continue;
if( cmp->HasChanged() )
{
return true;
}
}
}
return false;
}
/**
* Returns a list of only those components that have been changed
*/
std::vector<SCH_REFERENCE> BOM_TABLE_MODEL::GetChangedComponents()
{
std::vector<SCH_REFERENCE> components;
for( auto& group : Groups )
{
if( !group )
continue;
for( auto& component : group->Components )
{
if( !component )
continue;
if( component->HasChanged() )
{
for( auto& unit : component->Units )
{
components.push_back( unit );
}
}
}
}
return components;
}
/**
* Returns a count of the components that have been changed
*/
unsigned int BOM_TABLE_MODEL::CountChangedComponents()
{
unsigned int count = 0;
for( auto& group : Groups )
{
if( !group )
continue;
for( auto& component : group->Components )
{
if( component && component->HasChanged() )
{
count++;
}
}
}
return count;
}