mirror of
https://gitlab.com/kicad/code/kicad.git
synced 2025-09-14 02:03:12 +02:00
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
368 lines
10 KiB
C++
368 lines
10 KiB
C++
/*
|
|
* This program source code file is part of KiCad, a free EDA CAD application.
|
|
*lib_tree_model
|
|
* Copyright (C) 2017 Chris Pavlina <pavlina.chris@gmail.com>
|
|
* Copyright (C) 2014 Henner Zeller <h.zeller@acm.org>
|
|
* Copyright (C) 2014-2019 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 <lib_tree_model.h>
|
|
|
|
#include <algorithm>
|
|
#include <eda_pattern_match.h>
|
|
#include <lib_tree_item.h>
|
|
#include <utility>
|
|
#include <pgm_base.h>
|
|
#include <string_utils.h>
|
|
|
|
// Each node gets this lowest score initially, without any matches applied.
|
|
// Matches will then increase this score depending on match quality. This way,
|
|
// an empty search string will result in all components being displayed as they
|
|
// have the minimum score. However, in that case, we avoid expanding all the
|
|
// nodes asd the result is very unspecific.
|
|
static const unsigned kLowestDefaultScore = 1;
|
|
|
|
|
|
// Creates a score depending on the position of a string match. If the position
|
|
// is 0 (= prefix match), this returns the maximum score. This degrades until
|
|
// pos == max, which returns a score of 0; Evertyhing else beyond that is just
|
|
// 0. Only values >= 0 allowed for position and max.
|
|
//
|
|
// @param aPosition is the position a string has been found in a substring.
|
|
// @param aMaximum is the maximum score this function returns.
|
|
// @return position dependent score.
|
|
static int matchPosScore(int aPosition, int aMaximum)
|
|
{
|
|
return ( aPosition < aMaximum ) ? aMaximum - aPosition : 0;
|
|
}
|
|
|
|
|
|
void LIB_TREE_NODE::ResetScore()
|
|
{
|
|
for( std::unique_ptr<LIB_TREE_NODE>& child: m_Children )
|
|
child->ResetScore();
|
|
|
|
m_Score = kLowestDefaultScore;
|
|
}
|
|
|
|
|
|
void LIB_TREE_NODE::AssignIntrinsicRanks( bool presorted )
|
|
{
|
|
std::vector<LIB_TREE_NODE*> sort_buf;
|
|
|
|
if( presorted )
|
|
{
|
|
int max = m_Children.size() - 1;
|
|
|
|
for( int i = 0; i <= max; ++i )
|
|
m_Children[i]->m_IntrinsicRank = max - i;
|
|
}
|
|
else
|
|
{
|
|
for( std::unique_ptr<LIB_TREE_NODE>& child: m_Children )
|
|
sort_buf.push_back( child.get() );
|
|
|
|
std::sort( sort_buf.begin(), sort_buf.end(),
|
|
[]( LIB_TREE_NODE* a, LIB_TREE_NODE* b ) -> bool
|
|
{
|
|
return StrNumCmp( a->m_Name, b->m_Name, true ) > 0;
|
|
} );
|
|
|
|
for( int i = 0; i < (int) sort_buf.size(); ++i )
|
|
sort_buf[i]->m_IntrinsicRank = i;
|
|
}
|
|
}
|
|
|
|
|
|
void LIB_TREE_NODE::SortNodes()
|
|
{
|
|
std::sort( m_Children.begin(), m_Children.end(),
|
|
[]( std::unique_ptr<LIB_TREE_NODE>& a, std::unique_ptr<LIB_TREE_NODE>& b )
|
|
{
|
|
return Compare( *a, *b ) > 0;
|
|
} );
|
|
|
|
for( std::unique_ptr<LIB_TREE_NODE>& node: m_Children )
|
|
node->SortNodes();
|
|
}
|
|
|
|
|
|
int LIB_TREE_NODE::Compare( LIB_TREE_NODE const& aNode1, LIB_TREE_NODE const& aNode2 )
|
|
{
|
|
if( aNode1.m_Type != aNode2.m_Type )
|
|
return 0;
|
|
|
|
// Recently used sorts at top
|
|
if( aNode1.m_Name.StartsWith( wxT( "-- " ) ) )
|
|
return 1;
|
|
else if( aNode2.m_Name.StartsWith( wxT( "-- " ) ) )
|
|
return 0;
|
|
|
|
// Pinned nodes go next
|
|
if( aNode1.m_Pinned && !aNode2.m_Pinned )
|
|
return 1;
|
|
else if( aNode2.m_Pinned && !aNode1.m_Pinned )
|
|
return -1;
|
|
|
|
if( aNode1.m_Parent != aNode2.m_Parent )
|
|
return 0;
|
|
|
|
return aNode1.m_IntrinsicRank - aNode2.m_IntrinsicRank;
|
|
}
|
|
|
|
|
|
LIB_TREE_NODE::LIB_TREE_NODE()
|
|
: m_Parent( nullptr ),
|
|
m_Type( INVALID ),
|
|
m_IntrinsicRank( 0 ),
|
|
m_Score( kLowestDefaultScore ),
|
|
m_Pinned( false ),
|
|
m_Normalized( false ),
|
|
m_Unit( 0 ),
|
|
m_IsRoot( false )
|
|
{}
|
|
|
|
|
|
LIB_TREE_NODE_UNIT::LIB_TREE_NODE_UNIT( LIB_TREE_NODE* aParent, LIB_TREE_ITEM* aItem, int aUnit )
|
|
{
|
|
static void* locale = nullptr;
|
|
static wxString namePrefix;
|
|
|
|
// Fetching translations can take a surprising amount of time when loading libraries,
|
|
// so only do it when necessary.
|
|
if( Pgm().GetLocale() != locale )
|
|
{
|
|
namePrefix = _( "Unit" );
|
|
locale = Pgm().GetLocale();
|
|
}
|
|
|
|
m_Parent = aParent;
|
|
m_Type = UNIT;
|
|
|
|
m_Unit = aUnit;
|
|
m_LibId = aParent->m_LibId;
|
|
|
|
m_Name = namePrefix + " " + aItem->GetUnitReference( aUnit );
|
|
m_Desc = wxEmptyString;
|
|
m_MatchName = wxEmptyString;
|
|
|
|
m_IntrinsicRank = -aUnit;
|
|
}
|
|
|
|
|
|
LIB_TREE_NODE_LIB_ID::LIB_TREE_NODE_LIB_ID( LIB_TREE_NODE* aParent, LIB_TREE_ITEM* aItem )
|
|
{
|
|
m_Type = LIBID;
|
|
m_Parent = aParent;
|
|
|
|
m_LibId.SetLibNickname( aItem->GetLibNickname() );
|
|
m_LibId.SetLibItemName( aItem->GetName() );
|
|
|
|
m_Name = aItem->GetName();
|
|
m_Desc = aItem->GetDescription();
|
|
m_Footprint = aItem->GetFootprint();
|
|
|
|
aItem->GetChooserFields( m_Fields );
|
|
|
|
m_MatchName = aItem->GetName();
|
|
m_SearchText = aItem->GetSearchText();
|
|
m_Normalized = false;
|
|
|
|
m_IsRoot = aItem->IsRoot();
|
|
|
|
if( aItem->GetUnitCount() > 1 )
|
|
{
|
|
for( int u = 1; u <= aItem->GetUnitCount(); ++u )
|
|
AddUnit( aItem, u );
|
|
}
|
|
}
|
|
|
|
|
|
LIB_TREE_NODE_UNIT& LIB_TREE_NODE_LIB_ID::AddUnit( LIB_TREE_ITEM* aItem, int aUnit )
|
|
{
|
|
LIB_TREE_NODE_UNIT* unit = new LIB_TREE_NODE_UNIT( this, aItem, aUnit );
|
|
m_Children.push_back( std::unique_ptr<LIB_TREE_NODE>( unit ) );
|
|
return *unit;
|
|
}
|
|
|
|
|
|
void LIB_TREE_NODE_LIB_ID::Update( LIB_TREE_ITEM* aItem )
|
|
{
|
|
m_LibId.SetLibNickname( aItem->GetLibId().GetLibNickname() );
|
|
m_LibId.SetLibItemName( aItem->GetName() );
|
|
|
|
m_Name = aItem->GetName();
|
|
m_Desc = aItem->GetDescription();
|
|
m_MatchName = aItem->GetName();
|
|
|
|
aItem->GetChooserFields( m_Fields );
|
|
|
|
m_SearchText = aItem->GetSearchText();
|
|
m_Normalized = false;
|
|
|
|
m_IsRoot = aItem->IsRoot();
|
|
m_Children.clear();
|
|
|
|
for( int u = 1; u <= aItem->GetUnitCount(); ++u )
|
|
AddUnit( aItem, u );
|
|
}
|
|
|
|
|
|
void LIB_TREE_NODE_LIB_ID::UpdateScore( EDA_COMBINED_MATCHER& aMatcher, const wxString& aLib )
|
|
{
|
|
if( m_Score <= 0 )
|
|
return; // Leaf nodes without scores are out of the game.
|
|
|
|
if( !m_Normalized )
|
|
{
|
|
m_MatchName = UnescapeString( m_MatchName ).Lower();
|
|
m_SearchText = m_SearchText.Lower();
|
|
m_Normalized = true;
|
|
}
|
|
|
|
if( !aLib.IsEmpty() && m_Parent->m_MatchName != aLib )
|
|
{
|
|
m_Score = 0;
|
|
return;
|
|
}
|
|
|
|
// Keywords and description we only count if the match string is at
|
|
// least two characters long. That avoids spurious, low quality
|
|
// matches. Most abbreviations are at three characters long.
|
|
int found_pos = EDA_PATTERN_NOT_FOUND;
|
|
int matchers_fired = 0;
|
|
|
|
if( aMatcher.GetPattern() == m_MatchName )
|
|
{
|
|
m_Score += 1000; // exact match. High score :)
|
|
}
|
|
else if( aMatcher.Find( m_MatchName, matchers_fired, found_pos ) )
|
|
{
|
|
// Substring match. The earlier in the string the better.
|
|
m_Score += matchPosScore( found_pos, 20 ) + 20;
|
|
}
|
|
else if( aMatcher.Find( m_Parent->m_MatchName, matchers_fired, found_pos ) )
|
|
{
|
|
m_Score += 19; // parent name matches. score += 19
|
|
}
|
|
else if( aMatcher.Find( m_SearchText, matchers_fired, found_pos ) )
|
|
{
|
|
// If we have a very short search term (like one or two letters),
|
|
// we don't want to accumulate scores if they just happen to be in
|
|
// keywords or description as almost any one or two-letter
|
|
// combination shows up in there.
|
|
if( aMatcher.GetPattern().length() >= 2 )
|
|
{
|
|
// For longer terms, we add scores 1..18 for positional match
|
|
// (higher in the front, where the keywords are).
|
|
m_Score += matchPosScore( found_pos, 17 ) + 1;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// No match. That's it for this item.
|
|
m_Score = 0;
|
|
}
|
|
|
|
// More matchers = better match
|
|
m_Score += 2 * matchers_fired;
|
|
}
|
|
|
|
|
|
LIB_TREE_NODE_LIB::LIB_TREE_NODE_LIB( LIB_TREE_NODE* aParent, wxString const& aName,
|
|
wxString const& aDesc )
|
|
{
|
|
m_Type = LIB;
|
|
m_Name = aName;
|
|
m_MatchName = aName.Lower();
|
|
m_Desc = aDesc;
|
|
m_Parent = aParent;
|
|
m_LibId.SetLibNickname( aName );
|
|
}
|
|
|
|
|
|
LIB_TREE_NODE_LIB_ID& LIB_TREE_NODE_LIB::AddItem( LIB_TREE_ITEM* aItem )
|
|
{
|
|
LIB_TREE_NODE_LIB_ID* item = new LIB_TREE_NODE_LIB_ID( this, aItem );
|
|
m_Children.push_back( std::unique_ptr<LIB_TREE_NODE>( item ) );
|
|
return *item;
|
|
}
|
|
|
|
|
|
void LIB_TREE_NODE_LIB::UpdateScore( EDA_COMBINED_MATCHER& aMatcher, const wxString& aLib )
|
|
{
|
|
m_Score = 0;
|
|
|
|
// We need to score leaf nodes, which are usually (but not always) children.
|
|
|
|
if( m_Children.size() )
|
|
{
|
|
for( std::unique_ptr<LIB_TREE_NODE>& child: m_Children )
|
|
{
|
|
child->UpdateScore( aMatcher, aLib );
|
|
m_Score = std::max( m_Score, child->m_Score );
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// No children; we are a leaf.
|
|
|
|
if( !aLib.IsEmpty() )
|
|
{
|
|
m_Score = m_MatchName == aLib ? 1000 : 0;
|
|
return;
|
|
}
|
|
|
|
int found_pos = EDA_PATTERN_NOT_FOUND;
|
|
int matchers_fired = 0;
|
|
|
|
if( aMatcher.GetPattern() == m_MatchName )
|
|
{
|
|
m_Score += 1000; // exact match. High score :)
|
|
}
|
|
else if( aMatcher.Find( m_MatchName, matchers_fired, found_pos ) )
|
|
{
|
|
// Substring match. The earlier in the string the better.
|
|
m_Score += matchPosScore( found_pos, 20 ) + 20;
|
|
}
|
|
|
|
// More matchers = better match
|
|
m_Score += 2 * matchers_fired;
|
|
}
|
|
}
|
|
|
|
|
|
LIB_TREE_NODE_ROOT::LIB_TREE_NODE_ROOT()
|
|
{
|
|
m_Type = ROOT;
|
|
}
|
|
|
|
|
|
LIB_TREE_NODE_LIB& LIB_TREE_NODE_ROOT::AddLib( wxString const& aName, wxString const& aDesc )
|
|
{
|
|
LIB_TREE_NODE_LIB* lib = new LIB_TREE_NODE_LIB( this, aName, aDesc );
|
|
m_Children.push_back( std::unique_ptr<LIB_TREE_NODE>( lib ) );
|
|
return *lib;
|
|
}
|
|
|
|
|
|
void LIB_TREE_NODE_ROOT::UpdateScore( EDA_COMBINED_MATCHER& aMatcher, const wxString& aLib )
|
|
{
|
|
for( std::unique_ptr<LIB_TREE_NODE>& child: m_Children )
|
|
child->UpdateScore( aMatcher, aLib );
|
|
}
|
|
|