kicad-source/common/lib_tree_model_adapter.cpp
Jon Evans e294fe2074 ADDED: Dynamic field columns in symbol chooser
CHANGED: Symbol chooser search now considers custom symbol fields

Visible columns can be controlled in database libraries.
In standard KiCad libraries, we show columns for all custom fields for now.

Customizable column visibility will be added in the future.

Fixes https://gitlab.com/kicad/code/kicad/-/issues/11946
2022-09-05 16:38:14 -04:00

671 lines
19 KiB
C++

/*
* This program source code file is part of KiCad, a free EDA CAD application.
*
* Copyright (C) 2017 Chris Pavlina <pavlina.chris@gmail.com>
* Copyright (C) 2014 Henner Zeller <h.zeller@acm.org>
* Copyright (C) 2014-2021 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 <eda_base_frame.h>
#include <eda_pattern_match.h>
#include <kiface_base.h>
#include <config_params.h>
#include <lib_tree_model_adapter.h>
#include <project/project_file.h>
#include <settings/app_settings.h>
#include <widgets/ui_common.h>
#include <wx/tokenzr.h>
#include <wx/wupdlock.h>
#include <string_utils.h>
#define PINNED_ITEMS_KEY wxT( "PinnedItems" )
static const int kDataViewIndent = 20;
wxDataViewItem LIB_TREE_MODEL_ADAPTER::ToItem( const LIB_TREE_NODE* aNode )
{
return wxDataViewItem( const_cast<void*>( static_cast<void const*>( aNode ) ) );
}
LIB_TREE_NODE* LIB_TREE_MODEL_ADAPTER::ToNode( wxDataViewItem aItem )
{
return static_cast<LIB_TREE_NODE*>( aItem.GetID() );
}
unsigned int LIB_TREE_MODEL_ADAPTER::IntoArray( const LIB_TREE_NODE& aNode,
wxDataViewItemArray& aChildren )
{
unsigned int n = 0;
for( std::unique_ptr<LIB_TREE_NODE> const& child: aNode.m_Children )
{
if( child->m_Score > 0 )
{
aChildren.Add( ToItem( &*child ) );
++n;
}
}
return n;
}
LIB_TREE_MODEL_ADAPTER::LIB_TREE_MODEL_ADAPTER( EDA_BASE_FRAME* aParent,
const wxString& aPinnedKey ) :
m_parent( aParent ),
m_filter( SYM_FILTER_NONE ),
m_show_units( true ),
m_preselect_unit( 0 ),
m_freeze( 0 ),
m_widget( nullptr )
{
// Default column widths. Do not translate these names.
m_colWidths[ wxT( "Item" ) ] = 300;
m_colWidths[ wxT( "Description" ) ] = 600;
APP_SETTINGS_BASE* cfg = Kiface().KifaceSettings();
for( const std::pair<const wxString, int>& pair : cfg->m_LibTree.column_widths )
m_colWidths[pair.first] = pair.second;
}
LIB_TREE_MODEL_ADAPTER::~LIB_TREE_MODEL_ADAPTER()
{}
void LIB_TREE_MODEL_ADAPTER::SaveColWidths()
{
if( m_widget )
{
APP_SETTINGS_BASE* cfg = Kiface().KifaceSettings();
cfg->m_LibTree.columns.clear();
cfg->m_LibTree.column_widths.clear();
// TODO(JE) ordering?
for( const std::pair<const wxString, wxDataViewColumn*>& pair : m_colNameMap )
{
cfg->m_LibTree.columns.emplace_back( pair.first );
cfg->m_LibTree.column_widths[pair.first] = pair.second->GetWidth();
}
}
}
void LIB_TREE_MODEL_ADAPTER::SetFilter( SYM_FILTER_TYPE aFilter )
{
m_filter = aFilter;
}
void LIB_TREE_MODEL_ADAPTER::ShowUnits( bool aShow )
{
m_show_units = aShow;
}
void LIB_TREE_MODEL_ADAPTER::SetPreselectNode( const LIB_ID& aLibId, int aUnit )
{
m_preselect_lib_id = aLibId;
m_preselect_unit = aUnit;
}
LIB_TREE_NODE_LIB& LIB_TREE_MODEL_ADAPTER::DoAddLibraryNode( const wxString& aNodeName,
const wxString& aDesc, bool pinned )
{
LIB_TREE_NODE_LIB& lib_node = m_tree.AddLib( aNodeName, aDesc );
lib_node.m_Pinned = pinned;
return lib_node;
}
void LIB_TREE_MODEL_ADAPTER::DoAddLibrary( const wxString& aNodeName, const wxString& aDesc,
const std::vector<LIB_TREE_ITEM*>& aItemList,
bool pinned, bool presorted )
{
LIB_TREE_NODE_LIB& lib_node = DoAddLibraryNode( aNodeName, aDesc, pinned );
for( LIB_TREE_ITEM* item: aItemList )
lib_node.AddItem( item );
lib_node.AssignIntrinsicRanks( presorted );
}
void LIB_TREE_MODEL_ADAPTER::UpdateSearchString( const wxString& aSearch, bool aState )
{
{
wxWindowUpdateLocker updateLock( m_widget );
// Even with the updateLock, wxWidgets sometimes ties its knickers in a knot trying to
// run a wxdataview_selection_changed_callback() on a row that has been deleted.
// https://bugs.launchpad.net/kicad/+bug/1756255
m_widget->UnselectAll();
// This collapse is required before the call to "Freeze()" below. Once Freeze()
// is called, GetParent() will return nullptr. While this works for some calls, it
// segfaults when we have any expanded elements b/c the sub units in the tree don't
// have explicit references that are maintained over a search
// The tree will be expanded again below when we get our matches
//
// Also note that this cannot happen when we have deleted a symbol as GTK will also
// iterate over the tree in this case and find a symbol that has an invalid link
// and crash https://gitlab.com/kicad/code/kicad/-/issues/6910
if( !aState && !aSearch.IsNull() && m_tree.m_Children.size() )
{
for( std::unique_ptr<LIB_TREE_NODE>& child: m_tree.m_Children )
m_widget->Collapse( wxDataViewItem( &*child ) );
}
// DO NOT REMOVE THE FREEZE/THAW. This freeze/thaw is a flag for this model adapter
// that tells it when it shouldn't trust any of the data in the model. When set, it will
// not return invalid data to the UI, since this invalid data can cause crashes.
// This is different than the update locker, which locks the UI aspects only.
Freeze();
BeforeReset();
m_tree.ResetScore();
wxStringTokenizer tokenizer( aSearch );
while( tokenizer.HasMoreTokens() )
{
wxString lib;
wxString term = tokenizer.GetNextToken().Lower();
if( term.Contains( ":" ) )
{
lib = term.BeforeFirst( ':' );
term = term.AfterFirst( ':' );
}
EDA_COMBINED_MATCHER matcher( term, CTX_LIBITEM );
m_tree.UpdateScore( matcher, lib );
}
m_tree.SortNodes();
AfterReset();
Thaw();
}
LIB_TREE_NODE* bestMatch = ShowResults();
if( !bestMatch )
bestMatch = ShowPreselect();
if( !bestMatch )
bestMatch = ShowSingleLibrary();
if( bestMatch )
{
wxDataViewItem item = wxDataViewItem( bestMatch );
m_widget->Select( item );
// Make sure the *parent* item is visible. The selected item is the
// first (shown) child of the parent. So it's always right below the parent,
// and this way the user can also see what library the selected part belongs to,
// without having a case where the selection is off the screen (unless the
// window is a single row high, which is unlikely)
//
// This also happens to circumvent https://bugs.launchpad.net/kicad/+bug/1804400
// which appears to be a GTK+3 bug.
{
wxDataViewItem parent = GetParent( item );
if( parent.IsOk() )
m_widget->EnsureVisible( parent );
}
m_widget->EnsureVisible( item );
}
}
void LIB_TREE_MODEL_ADAPTER::AttachTo( wxDataViewCtrl* aDataViewCtrl )
{
m_widget = aDataViewCtrl;
aDataViewCtrl->SetIndent( kDataViewIndent );
aDataViewCtrl->AssociateModel( this );
aDataViewCtrl->ClearColumns();
// These two columns are always added; other columns may be added by specific libraries.
// Do not use translated names here.
doAddColumn( wxT( "Item" ) );
// TODO(JE) make Description optional
doAddColumn( wxT( "Description" ) );
for( auto& it : m_colNameMap )
{
if( !it.second )
doAddColumn( it.first, false );
}
}
wxDataViewColumn* LIB_TREE_MODEL_ADAPTER::doAddColumn( const wxString& aHeader, bool aTranslate )
{
wxString translatedHeader = aTranslate ? wxGetTranslation( aHeader ) : aHeader;
// The extent of the text doesn't take into account the space on either side
// in the header, so artificially pad it
wxSize headerMinWidth = KIUI::GetTextSize( translatedHeader + wxT( "MMM" ), m_widget );
if( !m_colWidths.count( aHeader ) || m_colWidths[aHeader] < headerMinWidth.x )
m_colWidths[aHeader] = headerMinWidth.x;
int index = m_columns.size();
wxDataViewColumn* ret = m_widget->AppendTextColumn( translatedHeader, index,
wxDATAVIEW_CELL_INERT,
m_colWidths[aHeader] );
ret->SetMinWidth( headerMinWidth.x );
m_columns.emplace_back( ret );
m_colNameMap[aHeader] = ret;
m_colIdxMap[m_columns.size() - 1] = aHeader;
return ret;
}
void LIB_TREE_MODEL_ADAPTER::addColumnIfNecessary( const wxString& aHeader )
{
if( m_colNameMap.count( aHeader ) )
return;
// Columns will be created later
m_colNameMap[aHeader] = nullptr;
}
LIB_ID LIB_TREE_MODEL_ADAPTER::GetAliasFor( const wxDataViewItem& aSelection ) const
{
const LIB_TREE_NODE* node = ToNode( aSelection );
LIB_ID emptyId;
if( !node )
return emptyId;
return node->m_LibId;
}
int LIB_TREE_MODEL_ADAPTER::GetUnitFor( const wxDataViewItem& aSelection ) const
{
const LIB_TREE_NODE* node = ToNode( aSelection );
return node ? node->m_Unit : 0;
}
LIB_TREE_NODE::TYPE LIB_TREE_MODEL_ADAPTER::GetTypeFor( const wxDataViewItem& aSelection ) const
{
const LIB_TREE_NODE* node = ToNode( aSelection );
return node ? node->m_Type : LIB_TREE_NODE::INVALID;
}
LIB_TREE_NODE* LIB_TREE_MODEL_ADAPTER::GetTreeNodeFor( const wxDataViewItem& aSelection ) const
{
return ToNode( aSelection );
}
int LIB_TREE_MODEL_ADAPTER::GetItemCount() const
{
int n = 0;
for( const std::unique_ptr<LIB_TREE_NODE>& lib: m_tree.m_Children )
n += lib->m_Children.size();
return n;
}
wxDataViewItem LIB_TREE_MODEL_ADAPTER::FindItem( const LIB_ID& aLibId )
{
for( std::unique_ptr<LIB_TREE_NODE>& lib: m_tree.m_Children )
{
if( lib->m_Name != aLibId.GetLibNickname() )
continue;
// if part name is not specified, return the library node
if( aLibId.GetLibItemName() == "" )
return ToItem( lib.get() );
for( std::unique_ptr<LIB_TREE_NODE>& alias: lib->m_Children )
{
if( alias->m_Name == aLibId.GetLibItemName() )
return ToItem( alias.get() );
}
break; // could not find the part in the requested library
}
return wxDataViewItem();
}
unsigned int LIB_TREE_MODEL_ADAPTER::GetChildren( const wxDataViewItem& aItem,
wxDataViewItemArray& aChildren ) const
{
const LIB_TREE_NODE* node = ( aItem.IsOk() ? ToNode( aItem ) : &m_tree );
if( node->m_Type == LIB_TREE_NODE::TYPE::ROOT
|| node->m_Type == LIB_TREE_NODE::LIB
|| ( m_show_units && node->m_Type == LIB_TREE_NODE::TYPE::LIBID ) )
{
return IntoArray( *node, aChildren );
}
else
{
return 0;
}
}
void LIB_TREE_MODEL_ADAPTER::FinishTreeInitialization()
{
wxDataViewColumn* col = nullptr;
size_t idx = 0;
int totalWidth = 0;
wxString header;
for( ; idx < m_columns.size() - 1; idx++ )
{
col = m_columns[idx];
header = col->GetTitle();
wxASSERT( m_colWidths.count( header ) );
col->SetWidth( m_colWidths[header] );
totalWidth += col->GetWidth();
}
int remainingWidth = m_widget->GetSize().x - totalWidth;
header = m_columns[idx]->GetTitle();
m_columns[idx]->SetWidth( std::max( m_colWidths[header], remainingWidth ) );
}
void LIB_TREE_MODEL_ADAPTER::OnSize( wxSizeEvent& aEvent )
{
for( auto& it : m_colNameMap )
{
if( it.second == m_columns[0] )
{
// On GTK, this value in not immediately available, so don't
// set it to zero just because we haven't fully initialized
if( it.second->GetWidth() > 0 )
m_colWidths[it.first] = it.second->GetWidth();
continue;
}
wxASSERT( m_colWidths.count( it.first ) );
it.second->SetWidth( m_colWidths[it.first] );
}
// Mandatory in any wxSizeEvent handler:
aEvent.Skip();
}
void LIB_TREE_MODEL_ADAPTER::RefreshTree()
{
// Yes, this is an enormous hack. But it works on all platforms, it doesn't suffer
// the On^2 sorting issues that ItemChanged() does on OSX, and it doesn't lose the
// user's scroll position (which re-attaching or deleting/re-inserting columns does).
static int walk = 1;
std::vector<int> widths;
for( const wxDataViewColumn* col : m_columns )
widths.emplace_back( col->GetWidth() );
wxASSERT( widths.size() );
// Only use the widths read back if they are non-zero.
// GTK returns the displayed width of the column, which is not calculated immediately
if( widths[0] > 0 )
{
size_t i = 0;
for( auto& it : m_colNameMap )
m_colWidths[it.first] = widths[i++];
}
auto colIt = m_colWidths.begin();
colIt->second += walk;
colIt++;
if( colIt != m_colWidths.end() )
colIt->second -= walk;
for( auto& it : m_colNameMap )
{
if( it.second == m_columns[0] )
continue;
wxASSERT( m_colWidths.count( it.first ) );
it.second->SetWidth( m_colWidths[it.first] );
}
walk = -walk;
}
bool LIB_TREE_MODEL_ADAPTER::HasContainerColumns( const wxDataViewItem& aItem ) const
{
return IsContainer( aItem );
}
bool LIB_TREE_MODEL_ADAPTER::IsContainer( const wxDataViewItem& aItem ) const
{
LIB_TREE_NODE* node = ToNode( aItem );
return node ? node->m_Children.size() : true;
}
wxDataViewItem LIB_TREE_MODEL_ADAPTER::GetParent( const wxDataViewItem& aItem ) const
{
if( m_freeze )
return ToItem( nullptr );
LIB_TREE_NODE* node = ToNode( aItem );
LIB_TREE_NODE* parent = node ? node->m_Parent : nullptr;
// wxDataViewModel has no root node, but rather top-level elements have
// an invalid (null) parent.
if( !node || !parent || parent->m_Type == LIB_TREE_NODE::TYPE::ROOT )
return ToItem( nullptr );
else
return ToItem( parent );
}
void LIB_TREE_MODEL_ADAPTER::GetValue( wxVariant& aVariant,
const wxDataViewItem& aItem,
unsigned int aCol ) const
{
if( IsFrozen() )
{
aVariant = wxEmptyString;
return;
}
LIB_TREE_NODE* node = ToNode( aItem );
wxASSERT( node );
switch( aCol )
{
case NAME_COL:
aVariant = UnescapeString( node->m_Name );
break;
case DESC_COL:
aVariant = node->m_Desc;
break;
default:
{
wxCHECK_RET( m_colIdxMap.count( aCol ), wxT( "Invalid column in LIB_TREE_MODEL_ADAPTER" ) );
if( node->m_Fields.count( m_colIdxMap.at( aCol ) ) )
aVariant = node->m_Fields[m_colIdxMap.at( aCol )];
else
aVariant = wxEmptyString;
break;
}
}
}
bool LIB_TREE_MODEL_ADAPTER::GetAttr( const wxDataViewItem& aItem,
unsigned int aCol,
wxDataViewItemAttr& aAttr ) const
{
if( IsFrozen() )
return false;
LIB_TREE_NODE* node = ToNode( aItem );
wxASSERT( node );
if( node->m_Type != LIB_TREE_NODE::LIBID )
{
// Currently only aliases are formatted at all
return false;
}
if( !node->m_IsRoot && aCol == 0 )
{
// Names of non-root aliases are italicized
aAttr.SetItalic( true );
return true;
}
else
{
return false;
}
}
void LIB_TREE_MODEL_ADAPTER::Find( LIB_TREE_NODE& aNode,
std::function<bool( const LIB_TREE_NODE* )> aFunc,
LIB_TREE_NODE** aHighScore )
{
for( std::unique_ptr<LIB_TREE_NODE>& node: aNode.m_Children )
{
if( aFunc( &*node ) )
{
if( !(*aHighScore) || node->m_Score > (*aHighScore)->m_Score )
(*aHighScore) = &*node;
}
Find( *node, aFunc, aHighScore );
}
}
LIB_TREE_NODE* LIB_TREE_MODEL_ADAPTER::ShowResults()
{
LIB_TREE_NODE* highScore = nullptr;
Find( m_tree,
[]( LIB_TREE_NODE const* n )
{
// return leaf nodes with some level of matching
return n->m_Type == LIB_TREE_NODE::TYPE::LIBID && n->m_Score > 1;
},
&highScore );
if( highScore)
{
wxDataViewItem item = wxDataViewItem( highScore );
m_widget->ExpandAncestors( item );
}
return highScore;
}
LIB_TREE_NODE* LIB_TREE_MODEL_ADAPTER::ShowPreselect()
{
LIB_TREE_NODE* highScore = nullptr;
if( !m_preselect_lib_id.IsValid() )
return highScore;
Find( m_tree,
[&]( LIB_TREE_NODE const* n )
{
if( n->m_Type == LIB_TREE_NODE::LIBID && ( n->m_Children.empty() ||
!m_preselect_unit ) )
return m_preselect_lib_id == n->m_LibId;
else if( n->m_Type == LIB_TREE_NODE::UNIT && m_preselect_unit )
return m_preselect_lib_id == n->m_Parent->m_LibId &&
m_preselect_unit == n->m_Unit;
else
return false;
},
&highScore );
if( highScore)
{
wxDataViewItem item = wxDataViewItem( highScore );
m_widget->ExpandAncestors( item );
}
return highScore;
}
LIB_TREE_NODE* LIB_TREE_MODEL_ADAPTER::ShowSingleLibrary()
{
LIB_TREE_NODE* highScore = nullptr;
Find( m_tree,
[]( LIB_TREE_NODE const* n )
{
return n->m_Type == LIB_TREE_NODE::TYPE::LIBID &&
n->m_Parent->m_Parent->m_Children.size() == 1;
},
&highScore );
if( highScore)
{
wxDataViewItem item = wxDataViewItem( highScore );
m_widget->ExpandAncestors( item );
}
return highScore;
}