/* -*- c++ -*- * This program source code file is part of KiCad, a free EDA CAD application. * * Copyright (C) 2014 Henner Zeller * 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 #include #include #include #include #include #include #include #include #include // 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. wxTreeItemId 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_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::SetTree( wxTreeCtrl* aTree ) { m_tree = aTree; UpdateSearchTerm( wxEmptyString ); } 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( 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() ) { // Preformatting. Unfortunately, the tree widget doesn't have columns // and using tabs does not work very well or does not work at all // (depending on OS versions). So indent with spaces in fixed-font width. // The 98%-ile of length of strings found in the standard library is 15 // characters. Use this as a reasonable cut-off point for aligned indentation. // For the few component names longer than that, the description is indented a // bit more. // The max found in the default lib would be 20 characters, but that creates too // much visible whitespace for the less extreme component names. const int COLUMN_DESCR_POS = 15; const int indent_len = COLUMN_DESCR_POS - a->GetName().length(); display_info = wxString::Format( wxT( " %*s [ %s ]" ), indent_len > 0 ? indent_len : 0, wxT( "" ), GetChars( 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( 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 wxTreeItemId& 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 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 wxTreeItemId root_id = m_tree->AddRoot( wxEmptyString ); 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; wxString node_text; #if 0 // Node text with scoring information for debugging node_text.Printf( wxT("%s (s=%u)%s"), GetChars(node->DisplayName), node->MatchScore, GetChars( node->DisplayInfo )); #else node_text = node->DisplayName + node->DisplayInfo; #endif node->TreeId = m_tree->AppendItem( node->Parent ? node->Parent->TreeId : root_id, node_text ); // 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 ) ) { m_tree->SelectItem( first_match->TreeId ); m_tree->EnsureVisible( first_match->TreeId ); } else if( preselected_node ) { m_tree->SelectItem( preselected_node->TreeId ); m_tree->EnsureVisible( preselected_node->TreeId ); } 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 }