529 lines
19 KiB
C++
529 lines
19 KiB
C++
/* -*- c++ -*-
|
|
* This program source code file is part of KiCad, a free EDA CAD application.
|
|
*
|
|
* Copyright (C) 2014 Henner Zeller <h.zeller@acm.org>
|
|
* Copyright (C) 2015-2017 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 <component_tree_search_container.h>
|
|
|
|
#include <algorithm>
|
|
#include <set>
|
|
|
|
#include <wx/string.h>
|
|
#include <wx/tokenzr.h>
|
|
#include <wx/arrstr.h>
|
|
#include <wx/dataview.h>
|
|
#include <widgets/two_column_tree_list.h>
|
|
|
|
#include <class_library.h>
|
|
#include <macros.h>
|
|
#include <profile.h>
|
|
|
|
#include <eda_pattern_match.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;
|
|
|
|
struct COMPONENT_TREE_SEARCH_CONTAINER::TREE_NODE
|
|
{
|
|
// Levels of nodes.
|
|
enum NODE_TYPE {
|
|
TYPE_LIB,
|
|
TYPE_ALIAS,
|
|
TYPE_UNIT
|
|
};
|
|
|
|
TREE_NODE(NODE_TYPE aType, TREE_NODE* aParent, LIB_ALIAS* aAlias,
|
|
const wxString& aName, const wxString& aDisplayInfo,
|
|
const wxString& aSearchText )
|
|
: Type( aType ),
|
|
Parent( aParent ), Alias( aAlias ), Unit( 0 ),
|
|
DisplayName( aName ),
|
|
DisplayInfo( aDisplayInfo ),
|
|
MatchName( aName.Lower() ),
|
|
SearchText( aSearchText.Lower() ),
|
|
MatchScore( 0 ), PreviousScore( 0 )
|
|
{
|
|
}
|
|
|
|
const NODE_TYPE Type; ///< Type of node in the hierarchy.
|
|
TREE_NODE* const Parent; ///< NULL if library, pointer to parent when component/alias.
|
|
LIB_ALIAS* const Alias; ///< Component alias associated with this entry.
|
|
int Unit; ///< Part number; Assigned: >= 1; default = 0
|
|
const wxString DisplayName; ///< Exact name as displayed to the user.
|
|
const wxString DisplayInfo; ///< Additional info displayed in the tree (description..)
|
|
|
|
const wxString MatchName; ///< Preprocessed: lowercased display name.
|
|
const wxString SearchText; ///< Other text (keywords, description..) to search in.
|
|
|
|
unsigned MatchScore; ///< Result-Score after UpdateSearchTerm()
|
|
unsigned PreviousScore; ///< Optimization: used to see if we need any tree update.
|
|
wxTreeListItem TreeId; ///< Tree-ID if stored in the tree (if MatchScore > 0).
|
|
};
|
|
|
|
|
|
// Sort tree nodes by reverse match-score (bigger is first), then alphabetically.
|
|
// Library (i.e. the ones that don't have a parent) are always sorted before any
|
|
// leaf nodes. Component
|
|
bool COMPONENT_TREE_SEARCH_CONTAINER::scoreComparator( const TREE_NODE* a1, const TREE_NODE* a2 )
|
|
{
|
|
if( a1->Type != a2->Type )
|
|
return a1->Type < a2->Type;
|
|
|
|
if( a1->MatchScore != a2->MatchScore )
|
|
return a1->MatchScore > a2->MatchScore; // biggest first.
|
|
|
|
if( a1->Parent != a2->Parent )
|
|
return scoreComparator( a1->Parent, a2->Parent );
|
|
|
|
return a1->MatchName.Cmp( a2->MatchName ) < 0;
|
|
}
|
|
|
|
|
|
COMPONENT_TREE_SEARCH_CONTAINER::COMPONENT_TREE_SEARCH_CONTAINER( PART_LIBS* aLibs )
|
|
: m_tree( NULL ),
|
|
m_libraries_added( 0 ),
|
|
m_components_added( 0 ),
|
|
m_preselect_unit_number( -1 ),
|
|
m_show_units( true ),
|
|
m_libs( aLibs ),
|
|
m_filter( CMP_FILTER_NONE )
|
|
{
|
|
}
|
|
|
|
|
|
COMPONENT_TREE_SEARCH_CONTAINER::~COMPONENT_TREE_SEARCH_CONTAINER()
|
|
{
|
|
for( TREE_NODE* node : m_nodes )
|
|
delete node;
|
|
|
|
m_nodes.clear();
|
|
}
|
|
|
|
|
|
void COMPONENT_TREE_SEARCH_CONTAINER::SetPreselectNode( const wxString& aComponentName,
|
|
int aUnit )
|
|
{
|
|
m_preselect_node_name = aComponentName.Lower();
|
|
m_preselect_unit_number = aUnit;
|
|
}
|
|
|
|
|
|
void COMPONENT_TREE_SEARCH_CONTAINER::ShowUnits( bool aShowUnits )
|
|
{
|
|
m_show_units = aShowUnits;
|
|
}
|
|
|
|
|
|
void COMPONENT_TREE_SEARCH_CONTAINER::SetTree( TWO_COLUMN_TREE_LIST* aTree )
|
|
{
|
|
m_tree = aTree;
|
|
|
|
if( m_tree )
|
|
{
|
|
m_tree->AppendColumn( _( "Part" ), wxCOL_WIDTH_AUTOSIZE, wxALIGN_LEFT, wxCOL_RESIZABLE );
|
|
m_tree->AppendColumn( _( "Description" ), 100, wxALIGN_LEFT, wxCOL_RESIZABLE );
|
|
m_tree->SetRubberBandColumn( 1 );
|
|
}
|
|
}
|
|
|
|
|
|
void COMPONENT_TREE_SEARCH_CONTAINER::AddLibrary( PART_LIB& aLib )
|
|
{
|
|
wxArrayString all_aliases;
|
|
|
|
if( m_filter == CMP_FILTER_POWER )
|
|
aLib.GetEntryTypePowerNames( all_aliases );
|
|
else
|
|
aLib.GetAliasNames( all_aliases );
|
|
|
|
AddAliasList( aLib.GetName(), all_aliases, &aLib );
|
|
|
|
++m_libraries_added;
|
|
}
|
|
|
|
|
|
void COMPONENT_TREE_SEARCH_CONTAINER::AddAliasList( const wxString& aNodeName,
|
|
const wxArrayString& aAliasNameList,
|
|
PART_LIB* aOptionalLib )
|
|
{
|
|
TREE_NODE* const lib_node = new TREE_NODE( TREE_NODE::TYPE_LIB, NULL, NULL,
|
|
aNodeName, wxEmptyString, wxEmptyString );
|
|
m_nodes.push_back( lib_node );
|
|
|
|
for( const wxString& aName : aAliasNameList )
|
|
{
|
|
LIB_ALIAS* a;
|
|
|
|
if( aOptionalLib )
|
|
a = aOptionalLib->FindAlias( aName );
|
|
else
|
|
a = m_libs->FindLibraryAlias( LIB_ID( wxEmptyString, aName ), wxEmptyString );
|
|
|
|
if( a == NULL )
|
|
continue;
|
|
|
|
wxString search_text;
|
|
search_text = ( a->GetKeyWords().empty() ) ? wxT(" ") : a->GetKeyWords();
|
|
search_text += a->GetDescription();
|
|
|
|
wxString display_info;
|
|
|
|
if( !a->GetDescription().empty() )
|
|
{
|
|
display_info = a->GetDescription();
|
|
}
|
|
|
|
TREE_NODE* alias_node = new TREE_NODE( TREE_NODE::TYPE_ALIAS, lib_node,
|
|
a, a->GetName(), display_info, search_text );
|
|
m_nodes.push_back( alias_node );
|
|
|
|
if( m_show_units && a->GetPart()->IsMulti() ) // Add all units as sub-nodes.
|
|
{
|
|
for( int u = 1; u <= a->GetPart()->GetUnitCount(); ++u )
|
|
{
|
|
wxString unitName = _( "Unit" );
|
|
unitName += wxT( " " ) + LIB_PART::SubReference( u, false );
|
|
TREE_NODE* unit_node = new TREE_NODE( TREE_NODE::TYPE_UNIT,
|
|
alias_node, a,
|
|
unitName,
|
|
wxEmptyString, wxEmptyString );
|
|
unit_node->Unit = u;
|
|
m_nodes.push_back( unit_node );
|
|
}
|
|
}
|
|
|
|
++m_components_added;
|
|
}
|
|
}
|
|
|
|
|
|
LIB_ALIAS* COMPONENT_TREE_SEARCH_CONTAINER::GetSelectedAlias( int* aUnit )
|
|
{
|
|
if( m_tree == NULL )
|
|
return NULL;
|
|
|
|
const wxTreeListItem& select_id = m_tree->GetSelection();
|
|
|
|
for( TREE_NODE* node : m_nodes )
|
|
{
|
|
if( node->MatchScore > 0 && node->TreeId == select_id )
|
|
{
|
|
if( aUnit && node->Unit > 0 )
|
|
*aUnit = node->Unit;
|
|
|
|
return node->Alias;
|
|
}
|
|
}
|
|
|
|
return NULL;
|
|
}
|
|
|
|
|
|
// 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;
|
|
}
|
|
|
|
|
|
namespace
|
|
{
|
|
class EDA_COMBINED_MATCHER
|
|
{
|
|
public:
|
|
EDA_COMBINED_MATCHER( const wxString &aPattern )
|
|
{
|
|
// Whatever syntax users prefer, it shall be matched.
|
|
AddMatcher( aPattern, new EDA_PATTERN_MATCH_REGEX() );
|
|
AddMatcher( aPattern, new EDA_PATTERN_MATCH_WILDCARD() );
|
|
// If any of the above matchers couldn't be created because the pattern
|
|
// syntax does not match, the substring will try its best.
|
|
AddMatcher( aPattern, new EDA_PATTERN_MATCH_SUBSTR() );
|
|
}
|
|
|
|
~EDA_COMBINED_MATCHER()
|
|
{
|
|
for( const EDA_PATTERN_MATCH* matcher : m_matchers )
|
|
delete matcher;
|
|
}
|
|
|
|
/*
|
|
* Look in all existing matchers, return the earliest match of any of
|
|
* the existing. Returns EDA_PATTERN_NOT_FOUND if no luck.
|
|
*/
|
|
int Find( const wxString &aTerm, int *aMatchersTriggered )
|
|
{
|
|
int result = EDA_PATTERN_NOT_FOUND;
|
|
|
|
for( const EDA_PATTERN_MATCH* matcher : m_matchers )
|
|
{
|
|
int local_find = matcher->Find( aTerm );
|
|
|
|
if ( local_find != EDA_PATTERN_NOT_FOUND )
|
|
{
|
|
*aMatchersTriggered += 1;
|
|
|
|
if ( local_find < result || result == EDA_PATTERN_NOT_FOUND )
|
|
{
|
|
result = local_find;
|
|
}
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
private:
|
|
// Add matcher if it can compile the pattern.
|
|
void AddMatcher( const wxString &aPattern, EDA_PATTERN_MATCH *aMatcher )
|
|
{
|
|
if ( aMatcher->SetPattern( aPattern ) )
|
|
{
|
|
m_matchers.push_back( aMatcher );
|
|
}
|
|
else
|
|
{
|
|
delete aMatcher;
|
|
}
|
|
}
|
|
|
|
std::vector<const EDA_PATTERN_MATCH*> m_matchers;
|
|
};
|
|
}
|
|
|
|
|
|
void COMPONENT_TREE_SEARCH_CONTAINER::UpdateSearchTerm( const wxString& aSearch )
|
|
{
|
|
if( m_tree == NULL )
|
|
return;
|
|
//#define SHOW_CALC_TIME // uncomment this to show calculation time
|
|
|
|
#ifdef SHOW_CALC_TIME
|
|
unsigned starttime = GetRunningMicroSecs();
|
|
#endif
|
|
|
|
// We score the list by going through it several time, essentially with a complexity
|
|
// of O(n). For the default library of 2000+ items, this typically takes less than 5ms
|
|
// on an i5. Good enough, no index needed.
|
|
|
|
// Initial AND condition: Leaf nodes are considered to match initially.
|
|
for( TREE_NODE* node : m_nodes )
|
|
{
|
|
node->PreviousScore = node->MatchScore;
|
|
node->MatchScore = ( node->Type == TREE_NODE::TYPE_LIB ) ? 0 : kLowestDefaultScore;
|
|
}
|
|
|
|
// Create match scores for each node for all the terms, that come space-separated.
|
|
// Scoring adds up values for each term according to importance of the match. If a term does
|
|
// not match at all, the result is thrown out of the results (AND semantics).
|
|
// From high to low
|
|
// - Exact match for a ccmponent name gives the highest score, trumping all.
|
|
// - A positional score depending of where a term is found as substring; prefix-match: high.
|
|
// - substring-match in library name.
|
|
// - substring match in keywords and descriptions with positional score. Keywords come
|
|
// first so contribute more to the score.
|
|
//
|
|
// This is of course subject to tweaking.
|
|
wxStringTokenizer tokenizer( aSearch );
|
|
|
|
while ( tokenizer.HasMoreTokens() )
|
|
{
|
|
const wxString term = tokenizer.GetNextToken().Lower();
|
|
EDA_COMBINED_MATCHER matcher( term );
|
|
|
|
for( TREE_NODE* node : m_nodes )
|
|
{
|
|
if( node->Type != TREE_NODE::TYPE_ALIAS )
|
|
continue; // Only aliases are actually scored here.
|
|
|
|
if( node->MatchScore == 0)
|
|
continue; // Leaf node without score are out of the game.
|
|
|
|
// 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;
|
|
int matcher_fired = 0;
|
|
|
|
if( term == node->MatchName )
|
|
node->MatchScore += 1000; // exact match. High score :)
|
|
else if( (found_pos = matcher.Find( node->MatchName, &matcher_fired ) ) != EDA_PATTERN_NOT_FOUND )
|
|
{
|
|
// Substring match. The earlier in the string the better. score += 20..40
|
|
node->MatchScore += matchPosScore( found_pos, 20 ) + 20;
|
|
}
|
|
else if( matcher.Find( node->Parent->MatchName, &matcher_fired ) != EDA_PATTERN_NOT_FOUND )
|
|
node->MatchScore += 19; // parent name matches. score += 19
|
|
else if( ( found_pos = matcher.Find( node->SearchText, &matcher_fired ) ) != EDA_PATTERN_NOT_FOUND )
|
|
{
|
|
// 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.
|
|
// For longer terms, we add scores 1..18 for positional match (higher in the
|
|
// front, where the keywords are). score += 0..18
|
|
node->MatchScore += ( ( term.length() >= 2 )
|
|
? matchPosScore( found_pos, 17 ) + 1
|
|
: 0 );
|
|
}
|
|
else
|
|
node->MatchScore = 0; // No match. That's it for this item.
|
|
|
|
node->MatchScore += 2 * matcher_fired;
|
|
}
|
|
}
|
|
|
|
// Library nodes have the maximum score seen in any of their children.
|
|
// Alias nodes have the score of their parents.
|
|
unsigned highest_score_seen = 0;
|
|
bool any_change = false;
|
|
|
|
for( TREE_NODE* node : m_nodes )
|
|
{
|
|
switch( node->Type )
|
|
{
|
|
case TREE_NODE::TYPE_ALIAS:
|
|
{
|
|
any_change |= (node->PreviousScore != node->MatchScore);
|
|
// Update library score.
|
|
node->Parent->MatchScore = std::max( node->Parent->MatchScore, node->MatchScore );
|
|
highest_score_seen = std::max( highest_score_seen, node->MatchScore );
|
|
}
|
|
break;
|
|
|
|
case TREE_NODE::TYPE_UNIT:
|
|
node->MatchScore = node->Parent->MatchScore;
|
|
break;
|
|
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
// The tree update might be slow, so we want to bail out if there is no change.
|
|
if( !any_change )
|
|
return;
|
|
|
|
// Now: sort all items according to match score, libraries first.
|
|
std::sort( m_nodes.begin(), m_nodes.end(), scoreComparator );
|
|
|
|
#ifdef SHOW_CALC_TIME
|
|
unsigned sorttime = GetRunningMicroSecs();
|
|
#endif
|
|
|
|
// Fill the tree with all items that have a match. Re-arranging, adding and removing changed
|
|
// items is pretty complex, so we just re-build the whole tree.
|
|
m_tree->Freeze();
|
|
m_tree->DeleteAllItems();
|
|
const wxTreeListItem root_id = m_tree->GetRootItem();
|
|
const TREE_NODE* first_match = NULL;
|
|
const TREE_NODE* preselected_node = NULL;
|
|
bool override_preselect = false;
|
|
|
|
for( TREE_NODE* node : m_nodes )
|
|
{
|
|
if( node->MatchScore == 0 )
|
|
continue;
|
|
|
|
// If we have nodes that go beyond the default score, suppress nodes that
|
|
// have the default score. That can happen if they have an honary += 0 score due to
|
|
// some one-letter match in the keyword or description. In this case, we prefer matches
|
|
// that just have higher scores. Improves relevancy and performance as the tree has to
|
|
// display less items.
|
|
if( highest_score_seen > kLowestDefaultScore && node->MatchScore == kLowestDefaultScore )
|
|
continue;
|
|
|
|
node->TreeId = m_tree->AppendItem( node->Parent ? node->Parent->TreeId : root_id,
|
|
node->DisplayName );
|
|
m_tree->SetItemText( node->TreeId, 1, node->DisplayInfo );
|
|
|
|
// If there is only a single library in this container, we want to have it
|
|
// unfolded (example: power library, libedit)
|
|
if( node->Type == TREE_NODE::TYPE_ALIAS && m_libraries_added == 1 )
|
|
{
|
|
m_tree->Expand( node->TreeId );
|
|
|
|
if( first_match == NULL )
|
|
first_match = node;
|
|
}
|
|
|
|
// If we are a nicely scored alias, we want to have it visible.
|
|
if( node->Type == TREE_NODE::TYPE_ALIAS && ( node->MatchScore > kLowestDefaultScore ) )
|
|
{
|
|
m_tree->Expand( node->TreeId );
|
|
|
|
if( first_match == NULL )
|
|
first_match = node; // First, highest scoring: the "I am feeling lucky" element.
|
|
|
|
// The user is searching, don't preselect!
|
|
override_preselect = true;
|
|
}
|
|
|
|
// The first node that matches our pre-select criteria is choosen. 'First node'
|
|
// means, it shows up in the history, as the history node is displayed very first
|
|
// (by virtue of alphabetical ordering)
|
|
if( preselected_node == NULL
|
|
&& node->Type == TREE_NODE::TYPE_ALIAS
|
|
&& node->MatchName == m_preselect_node_name )
|
|
preselected_node = node;
|
|
|
|
// Refinement in case we come accross a matching unit node.
|
|
if( preselected_node != NULL && preselected_node->Type == TREE_NODE::TYPE_ALIAS
|
|
&& node->Parent == preselected_node
|
|
&& m_preselect_unit_number >= 1 && node->Unit == m_preselect_unit_number )
|
|
preselected_node = node;
|
|
}
|
|
|
|
if( first_match && ( !preselected_node || override_preselect ) )
|
|
{
|
|
// This is the wx call that pumps the event loop on some ports (namely
|
|
// macOS). TODO: find a way to avoid that.
|
|
m_tree->Select( first_match->TreeId );
|
|
//m_tree->EnsureVisible( first_match->TreeId );
|
|
}
|
|
else if( preselected_node )
|
|
{
|
|
// This is the wx call that pumps the event loop on some ports (namely
|
|
// macOS). TODO: find a way to avoid that.
|
|
m_tree->Select( preselected_node->TreeId );
|
|
//m_tree->EnsureVisible( preselected_node->TreeId );
|
|
}
|
|
|
|
m_tree->AutosizeColumns();
|
|
m_tree->Thaw();
|
|
|
|
#ifdef SHOW_CALC_TIME
|
|
unsigned endtime = GetRunningMicroSecs();
|
|
wxLogMessage( wxT("sort components %.1f ms, rebuild tree %.1f ms"),
|
|
double(sorttime-starttime)/1000.0, double(endtime-sorttime)/1000.0 );
|
|
#endif
|
|
}
|