Loading eeschema/CMakeLists.txt +3 −0 Original line number Original line Diff line number Diff line Loading @@ -19,6 +19,8 @@ set( EESCHEMA_DLGS dialogs/dialog_bom.cpp dialogs/dialog_bom.cpp dialogs/dialog_bom_base.cpp dialogs/dialog_bom_base.cpp dialogs/dialog_bom_cfg_keywords.cpp dialogs/dialog_bom_cfg_keywords.cpp dialogs/dialog_choose_component.cpp dialogs/dialog_choose_component_base.cpp dialogs/dialog_lib_edit_text.cpp dialogs/dialog_lib_edit_text.cpp dialogs/dialog_lib_edit_text_base.cpp dialogs/dialog_lib_edit_text_base.cpp dialogs/dialog_edit_component_in_lib.cpp dialogs/dialog_edit_component_in_lib.cpp Loading Loading @@ -88,6 +90,7 @@ set( EESCHEMA_SRCS files-io.cpp files-io.cpp find.cpp find.cpp getpart.cpp getpart.cpp component_tree_search_container.cpp hierarch.cpp hierarch.cpp hotkeys.cpp hotkeys.cpp libarch.cpp libarch.cpp Loading eeschema/component_tree_search_container.cpp 0 → 100644 +356 −0 Original line number Original line Diff line number Diff line /* -*- 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) 2014 KiCad Developers, see change_log.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 <boost/foreach.hpp> #include <set> #include <wx/string.h> #include <wx/tokenzr.h> #include <wx/treectrl.h> #include <wx/arrstr.h> #include <class_library.h> #include <macros.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 { TREE_NODE(TREE_NODE* aParent, CMP_LIBRARY* aOwningLib, const wxString& aName, const wxString& aDisplayInfo, const wxString& aSearchText, bool aNormallyExpanded = false) : parent( aParent ), lib( aOwningLib ), normally_expanded( aNormallyExpanded ), name( aName ), display_info( aDisplayInfo ), match_name( aName.Lower() ), search_text( aSearchText.Lower() ), match_score( 0 ), previous_score( 0 ) { } TREE_NODE* parent; // NULL if library, pointer to lib when component. CMP_LIBRARY* lib; // Owning library of this component. bool normally_expanded; // If this is a parent node, should it be unfolded ? wxString name; // Exact name as displayed to the user. wxString display_info; // Additional info displayed in the tree (description..) wxString match_name; // Preprocessed: lowercased display name. wxString search_text; // Other lowercase text (keywords, description..) to search in. unsigned match_score; // Result-Score after UpdateSearchTerm() unsigned previous_score; // Optimization: used to see if we need any tree update. wxTreeItemId tree_id; // Tree-ID if stored in the tree (if match_score > 0). }; // Sort tree nodes by reverse match-score (bigger is first), then alphabetically. // Library nodes (i.e. the ones that don't have a parent) are always sorted before any // leaf nodes. bool COMPONENT_TREE_SEARCH_CONTAINER::scoreComparator( const TREE_NODE* a1, const TREE_NODE* a2 ) { if ( a1->parent == NULL && a2->parent != NULL ) return true; if ( a1->parent != NULL && a2->parent == NULL ) return false; if ( a1->match_score != a2->match_score ) return a1->match_score > a2->match_score; // biggest first. if (a1->parent != a2->parent) return a1->parent->match_name.Cmp(a2->parent->match_name) < 0; return a1->match_name.Cmp( a2->match_name ) < 0; } COMPONENT_TREE_SEARCH_CONTAINER::COMPONENT_TREE_SEARCH_CONTAINER() : tree( NULL ) { } COMPONENT_TREE_SEARCH_CONTAINER::~COMPONENT_TREE_SEARCH_CONTAINER() { BOOST_FOREACH( TREE_NODE* node, nodes ) delete node; nodes.clear(); } void COMPONENT_TREE_SEARCH_CONTAINER::SetPreselectNode( const wxString& aComponentName ) { preselect_node_name = aComponentName.Lower(); } void COMPONENT_TREE_SEARCH_CONTAINER::SetTree( wxTreeCtrl* aTree ) { tree = aTree; UpdateSearchTerm( wxEmptyString ); } void COMPONENT_TREE_SEARCH_CONTAINER::AddLibrary( CMP_LIBRARY& aLib ) { wxArrayString all_comp; aLib.GetEntryNames( all_comp ); AddComponentList( aLib.GetName(), all_comp, &aLib, false ); } void COMPONENT_TREE_SEARCH_CONTAINER::AddComponentList( const wxString& aNodeName, const wxArrayString& aComponentNameList, CMP_LIBRARY* aOptionalLib, bool aNormallyExpanded ) { TREE_NODE* parent_node = new TREE_NODE( NULL, NULL, aNodeName, wxEmptyString, wxEmptyString, aNormallyExpanded ); nodes.push_back( parent_node ); BOOST_FOREACH( const wxString& cName, aComponentNameList ) { LIB_COMPONENT *c; if (aOptionalLib) c = aOptionalLib->FindComponent( cName ); else c = CMP_LIBRARY::FindLibraryComponent( cName, wxEmptyString ); if (c == NULL) continue; wxString keywords, descriptions; wxString display_info; for ( size_t i = 0; i < c->GetAliasCount(); ++i ) { LIB_ALIAS *a = c->GetAlias( i ); keywords += a->GetKeyWords(); descriptions += a->GetDescription(); if ( display_info.empty() && !a->GetDescription().empty() ) { // Preformatting. Unfortunately, the tree widget doesn't have columns display_info.Printf( wxT(" %s[ %s ]"), ( cName.length() <= 8 ) ? wxT("\t\t") : wxT("\t"), GetChars( a->GetDescription() ) ); } } // If there are no keywords, we give a couple of characters whitespace penalty. We want // a component with a search-term found in the keywords score slightly higher than another // component without keywords, but that term in the descriptions. wxString search_text = ( !keywords.empty() ) ? keywords : wxT(" "); search_text += descriptions; nodes.push_back( new TREE_NODE( parent_node, c->GetLibrary(), cName, display_info, search_text ) ); } } LIB_COMPONENT* COMPONENT_TREE_SEARCH_CONTAINER::GetSelectedComponent() { const wxTreeItemId& select_id = tree->GetSelection(); BOOST_FOREACH( TREE_NODE* node, nodes ) { if ( node->match_score > 0 && node->tree_id == select_id && node->lib ) return node->lib->FindComponent( node->name ); } 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; } void COMPONENT_TREE_SEARCH_CONTAINER::UpdateSearchTerm( const wxString& aSearch ) { if ( tree == NULL ) return; // 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. BOOST_FOREACH( TREE_NODE* node, nodes ) { node->previous_score = node->match_score; node->match_score = node->parent ? kLowestDefaultScore : 0; // start-match for leafs. } // 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(); BOOST_FOREACH( TREE_NODE* node, nodes ) { if ( node->parent == NULL) continue; // Library nodes are not scored here. if ( node->match_score == 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; if ( term == node->match_name ) node->match_score += 1000; // exact match. High score :) else if ( (found_pos = node->match_name.Find( term ) ) != wxNOT_FOUND ) { // Substring match. The earlier in the string the better. score += 20..40 node->match_score += matchPosScore( found_pos, 20 ) + 20; } else if ( node->parent->match_name.Find( term ) != wxNOT_FOUND ) node->match_score += 19; // parent name matches. score += 19 else if ( ( found_pos = node->search_text.Find( term ) ) != wxNOT_FOUND ) { // If we have a very short string (like one or two letters), we don't want // to accumulate scores if they just happen to be in keywords or description. // For longer terms, we add scores 1..18 for positional match (higher in the // front, where the keywords are). score += 0..18 node->match_score += ( ( term.length() >= 2 ) ? matchPosScore( found_pos, 17 ) + 1 : 0 ); } else node->match_score = 0; // No match. That's it for this item. } } // Parent nodes have the maximum score seen in any of their children. unsigned highest_score_seen = 0; bool any_change = false; BOOST_FOREACH( TREE_NODE* node, nodes ) { if ( node->parent == NULL ) continue; any_change |= (node->previous_score != node->match_score); node->parent->match_score = std::max( node->parent->match_score, node->match_score ); highest_score_seen = std::max( highest_score_seen, node->match_score ); } // 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( nodes.begin(), nodes.end(), scoreComparator ); // 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. tree->Freeze(); tree->DeleteAllItems(); wxTreeItemId root = tree->AddRoot( wxEmptyString ); const TREE_NODE* first_match = NULL; const TREE_NODE* preselected_node = NULL; BOOST_FOREACH( TREE_NODE* node, nodes ) { if ( node->match_score == 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 confusion (and performance as the tree has to // display less items). if ( highest_score_seen > kLowestDefaultScore && node->match_score == kLowestDefaultScore ) continue; wxString node_text; #if 0 // Node text with scoring information for debugging node_text.Printf( wxT("%s (s=%u)%s"), GetChars(node->name), node->match_score, GetChars(node->display_info)); #else node_text = node->name + node->display_info; #endif node->tree_id = tree->AppendItem( ( node->parent == NULL ) ? root : node->parent->tree_id, node_text); // If we are a child node, we might need to expand. if ( node->parent != NULL ) { if ( node->match_score > kLowestDefaultScore ) { tree->EnsureVisible( node->tree_id ); if ( first_match == NULL ) first_match = node; // The "I am feeling lucky" element. } if ( preselected_node == NULL && node->match_name == preselect_node_name ) preselected_node = node; } if ( node->parent == NULL && node->normally_expanded ) tree->Expand( node->tree_id ); } if ( first_match ) // Highest score search match pre-selected. tree->SelectItem( first_match->tree_id ); else if ( preselected_node ) // No search, so history item preselected. tree->SelectItem( preselected_node->tree_id ); tree->Thaw(); } eeschema/component_tree_search_container.h 0 → 100644 +108 −0 Original line number Original line Diff line number Diff line /* -*- 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) 2014 KiCad Developers, see change_log.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 <vector> #include <wx/string.h> class LIB_COMPONENT; class CMP_LIBRARY; class wxTreeCtrl; class wxArrayString; // class COMPONENT_TREE_SEARCH_CONTAINER // A container for components that allows to search them matching their name, keywords // and descripotions, updating a wxTreeCtrl with the results (toplevel nodes: // libraries, leafs: components), scored by relevance. // // The scored result list is adpated on each update on the search-term: this allows // to have a search-as-you-type experience. class COMPONENT_TREE_SEARCH_CONTAINER { public: COMPONENT_TREE_SEARCH_CONTAINER(); ~COMPONENT_TREE_SEARCH_CONTAINER(); /** Function AddLibrary * Add the components of this library to be searched. * To be called in the setup phase to fill this container. * * @param aLib containting all the components to be added. */ void AddLibrary( CMP_LIBRARY& aLib ); /** Function AddComponentList * Add the given list of components, given by name, to be searched. * To be called in the setup phase to fill this container. * * @param aNodeName The parent node name the components will show up as leaf. * @param aComponentNameList List of component names. * @param aOptionalLib Library to look up the component names (if NULL: global lookup) * @param aNormallyExpanded Should the node in the tree be expanded by default. */ void AddComponentList( const wxString& aNodeName, const wxArrayString& aComponentNameList, CMP_LIBRARY* aOptionalLib, bool aNormallyExpanded ); /** Function SetPreselectNode * Set the component name to be selected in absence of any search-result. * * @param aComponentName the component name to be selected. */ void SetPreselectNode( const wxString& aComponentName ); /** Function SetTree * Set the tree to be manipulated. * Each update of the search term will update the tree, with the most * scoring component at the top and selected. If a preselect node is set, this * is displayed. Does not take ownership of the tree. * * @param aTree that is to be modified on search updates. */ void SetTree( wxTreeCtrl* aTree ); /** Function UpdateSearchTerm * Update the search string provided by the user and narrow down the result list. * * This string is a space-separated list of terms, each of which * is applied to the components list to narrow it down. Results are scored by * relevancy (e.g. exact match scores higher than prefix-match which in turn scores * higher than substring match). This updates the search and tree on each call. * * @param aSearch is the user-provided search string. */ void UpdateSearchTerm( const wxString& aSearch ); /** Function GetSelectedComponent * * @return the selected component or NULL if there is none. */ LIB_COMPONENT* GetSelectedComponent(); private: struct TREE_NODE; static bool scoreComparator( const TREE_NODE* a1, const TREE_NODE* a2 ); std::vector<TREE_NODE*> nodes; wxString preselect_node_name; wxTreeCtrl* tree; }; Loading
eeschema/CMakeLists.txt +3 −0 Original line number Original line Diff line number Diff line Loading @@ -19,6 +19,8 @@ set( EESCHEMA_DLGS dialogs/dialog_bom.cpp dialogs/dialog_bom.cpp dialogs/dialog_bom_base.cpp dialogs/dialog_bom_base.cpp dialogs/dialog_bom_cfg_keywords.cpp dialogs/dialog_bom_cfg_keywords.cpp dialogs/dialog_choose_component.cpp dialogs/dialog_choose_component_base.cpp dialogs/dialog_lib_edit_text.cpp dialogs/dialog_lib_edit_text.cpp dialogs/dialog_lib_edit_text_base.cpp dialogs/dialog_lib_edit_text_base.cpp dialogs/dialog_edit_component_in_lib.cpp dialogs/dialog_edit_component_in_lib.cpp Loading Loading @@ -88,6 +90,7 @@ set( EESCHEMA_SRCS files-io.cpp files-io.cpp find.cpp find.cpp getpart.cpp getpart.cpp component_tree_search_container.cpp hierarch.cpp hierarch.cpp hotkeys.cpp hotkeys.cpp libarch.cpp libarch.cpp Loading
eeschema/component_tree_search_container.cpp 0 → 100644 +356 −0 Original line number Original line Diff line number Diff line /* -*- 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) 2014 KiCad Developers, see change_log.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 <boost/foreach.hpp> #include <set> #include <wx/string.h> #include <wx/tokenzr.h> #include <wx/treectrl.h> #include <wx/arrstr.h> #include <class_library.h> #include <macros.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 { TREE_NODE(TREE_NODE* aParent, CMP_LIBRARY* aOwningLib, const wxString& aName, const wxString& aDisplayInfo, const wxString& aSearchText, bool aNormallyExpanded = false) : parent( aParent ), lib( aOwningLib ), normally_expanded( aNormallyExpanded ), name( aName ), display_info( aDisplayInfo ), match_name( aName.Lower() ), search_text( aSearchText.Lower() ), match_score( 0 ), previous_score( 0 ) { } TREE_NODE* parent; // NULL if library, pointer to lib when component. CMP_LIBRARY* lib; // Owning library of this component. bool normally_expanded; // If this is a parent node, should it be unfolded ? wxString name; // Exact name as displayed to the user. wxString display_info; // Additional info displayed in the tree (description..) wxString match_name; // Preprocessed: lowercased display name. wxString search_text; // Other lowercase text (keywords, description..) to search in. unsigned match_score; // Result-Score after UpdateSearchTerm() unsigned previous_score; // Optimization: used to see if we need any tree update. wxTreeItemId tree_id; // Tree-ID if stored in the tree (if match_score > 0). }; // Sort tree nodes by reverse match-score (bigger is first), then alphabetically. // Library nodes (i.e. the ones that don't have a parent) are always sorted before any // leaf nodes. bool COMPONENT_TREE_SEARCH_CONTAINER::scoreComparator( const TREE_NODE* a1, const TREE_NODE* a2 ) { if ( a1->parent == NULL && a2->parent != NULL ) return true; if ( a1->parent != NULL && a2->parent == NULL ) return false; if ( a1->match_score != a2->match_score ) return a1->match_score > a2->match_score; // biggest first. if (a1->parent != a2->parent) return a1->parent->match_name.Cmp(a2->parent->match_name) < 0; return a1->match_name.Cmp( a2->match_name ) < 0; } COMPONENT_TREE_SEARCH_CONTAINER::COMPONENT_TREE_SEARCH_CONTAINER() : tree( NULL ) { } COMPONENT_TREE_SEARCH_CONTAINER::~COMPONENT_TREE_SEARCH_CONTAINER() { BOOST_FOREACH( TREE_NODE* node, nodes ) delete node; nodes.clear(); } void COMPONENT_TREE_SEARCH_CONTAINER::SetPreselectNode( const wxString& aComponentName ) { preselect_node_name = aComponentName.Lower(); } void COMPONENT_TREE_SEARCH_CONTAINER::SetTree( wxTreeCtrl* aTree ) { tree = aTree; UpdateSearchTerm( wxEmptyString ); } void COMPONENT_TREE_SEARCH_CONTAINER::AddLibrary( CMP_LIBRARY& aLib ) { wxArrayString all_comp; aLib.GetEntryNames( all_comp ); AddComponentList( aLib.GetName(), all_comp, &aLib, false ); } void COMPONENT_TREE_SEARCH_CONTAINER::AddComponentList( const wxString& aNodeName, const wxArrayString& aComponentNameList, CMP_LIBRARY* aOptionalLib, bool aNormallyExpanded ) { TREE_NODE* parent_node = new TREE_NODE( NULL, NULL, aNodeName, wxEmptyString, wxEmptyString, aNormallyExpanded ); nodes.push_back( parent_node ); BOOST_FOREACH( const wxString& cName, aComponentNameList ) { LIB_COMPONENT *c; if (aOptionalLib) c = aOptionalLib->FindComponent( cName ); else c = CMP_LIBRARY::FindLibraryComponent( cName, wxEmptyString ); if (c == NULL) continue; wxString keywords, descriptions; wxString display_info; for ( size_t i = 0; i < c->GetAliasCount(); ++i ) { LIB_ALIAS *a = c->GetAlias( i ); keywords += a->GetKeyWords(); descriptions += a->GetDescription(); if ( display_info.empty() && !a->GetDescription().empty() ) { // Preformatting. Unfortunately, the tree widget doesn't have columns display_info.Printf( wxT(" %s[ %s ]"), ( cName.length() <= 8 ) ? wxT("\t\t") : wxT("\t"), GetChars( a->GetDescription() ) ); } } // If there are no keywords, we give a couple of characters whitespace penalty. We want // a component with a search-term found in the keywords score slightly higher than another // component without keywords, but that term in the descriptions. wxString search_text = ( !keywords.empty() ) ? keywords : wxT(" "); search_text += descriptions; nodes.push_back( new TREE_NODE( parent_node, c->GetLibrary(), cName, display_info, search_text ) ); } } LIB_COMPONENT* COMPONENT_TREE_SEARCH_CONTAINER::GetSelectedComponent() { const wxTreeItemId& select_id = tree->GetSelection(); BOOST_FOREACH( TREE_NODE* node, nodes ) { if ( node->match_score > 0 && node->tree_id == select_id && node->lib ) return node->lib->FindComponent( node->name ); } 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; } void COMPONENT_TREE_SEARCH_CONTAINER::UpdateSearchTerm( const wxString& aSearch ) { if ( tree == NULL ) return; // 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. BOOST_FOREACH( TREE_NODE* node, nodes ) { node->previous_score = node->match_score; node->match_score = node->parent ? kLowestDefaultScore : 0; // start-match for leafs. } // 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(); BOOST_FOREACH( TREE_NODE* node, nodes ) { if ( node->parent == NULL) continue; // Library nodes are not scored here. if ( node->match_score == 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; if ( term == node->match_name ) node->match_score += 1000; // exact match. High score :) else if ( (found_pos = node->match_name.Find( term ) ) != wxNOT_FOUND ) { // Substring match. The earlier in the string the better. score += 20..40 node->match_score += matchPosScore( found_pos, 20 ) + 20; } else if ( node->parent->match_name.Find( term ) != wxNOT_FOUND ) node->match_score += 19; // parent name matches. score += 19 else if ( ( found_pos = node->search_text.Find( term ) ) != wxNOT_FOUND ) { // If we have a very short string (like one or two letters), we don't want // to accumulate scores if they just happen to be in keywords or description. // For longer terms, we add scores 1..18 for positional match (higher in the // front, where the keywords are). score += 0..18 node->match_score += ( ( term.length() >= 2 ) ? matchPosScore( found_pos, 17 ) + 1 : 0 ); } else node->match_score = 0; // No match. That's it for this item. } } // Parent nodes have the maximum score seen in any of their children. unsigned highest_score_seen = 0; bool any_change = false; BOOST_FOREACH( TREE_NODE* node, nodes ) { if ( node->parent == NULL ) continue; any_change |= (node->previous_score != node->match_score); node->parent->match_score = std::max( node->parent->match_score, node->match_score ); highest_score_seen = std::max( highest_score_seen, node->match_score ); } // 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( nodes.begin(), nodes.end(), scoreComparator ); // 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. tree->Freeze(); tree->DeleteAllItems(); wxTreeItemId root = tree->AddRoot( wxEmptyString ); const TREE_NODE* first_match = NULL; const TREE_NODE* preselected_node = NULL; BOOST_FOREACH( TREE_NODE* node, nodes ) { if ( node->match_score == 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 confusion (and performance as the tree has to // display less items). if ( highest_score_seen > kLowestDefaultScore && node->match_score == kLowestDefaultScore ) continue; wxString node_text; #if 0 // Node text with scoring information for debugging node_text.Printf( wxT("%s (s=%u)%s"), GetChars(node->name), node->match_score, GetChars(node->display_info)); #else node_text = node->name + node->display_info; #endif node->tree_id = tree->AppendItem( ( node->parent == NULL ) ? root : node->parent->tree_id, node_text); // If we are a child node, we might need to expand. if ( node->parent != NULL ) { if ( node->match_score > kLowestDefaultScore ) { tree->EnsureVisible( node->tree_id ); if ( first_match == NULL ) first_match = node; // The "I am feeling lucky" element. } if ( preselected_node == NULL && node->match_name == preselect_node_name ) preselected_node = node; } if ( node->parent == NULL && node->normally_expanded ) tree->Expand( node->tree_id ); } if ( first_match ) // Highest score search match pre-selected. tree->SelectItem( first_match->tree_id ); else if ( preselected_node ) // No search, so history item preselected. tree->SelectItem( preselected_node->tree_id ); tree->Thaw(); }
eeschema/component_tree_search_container.h 0 → 100644 +108 −0 Original line number Original line Diff line number Diff line /* -*- 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) 2014 KiCad Developers, see change_log.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 <vector> #include <wx/string.h> class LIB_COMPONENT; class CMP_LIBRARY; class wxTreeCtrl; class wxArrayString; // class COMPONENT_TREE_SEARCH_CONTAINER // A container for components that allows to search them matching their name, keywords // and descripotions, updating a wxTreeCtrl with the results (toplevel nodes: // libraries, leafs: components), scored by relevance. // // The scored result list is adpated on each update on the search-term: this allows // to have a search-as-you-type experience. class COMPONENT_TREE_SEARCH_CONTAINER { public: COMPONENT_TREE_SEARCH_CONTAINER(); ~COMPONENT_TREE_SEARCH_CONTAINER(); /** Function AddLibrary * Add the components of this library to be searched. * To be called in the setup phase to fill this container. * * @param aLib containting all the components to be added. */ void AddLibrary( CMP_LIBRARY& aLib ); /** Function AddComponentList * Add the given list of components, given by name, to be searched. * To be called in the setup phase to fill this container. * * @param aNodeName The parent node name the components will show up as leaf. * @param aComponentNameList List of component names. * @param aOptionalLib Library to look up the component names (if NULL: global lookup) * @param aNormallyExpanded Should the node in the tree be expanded by default. */ void AddComponentList( const wxString& aNodeName, const wxArrayString& aComponentNameList, CMP_LIBRARY* aOptionalLib, bool aNormallyExpanded ); /** Function SetPreselectNode * Set the component name to be selected in absence of any search-result. * * @param aComponentName the component name to be selected. */ void SetPreselectNode( const wxString& aComponentName ); /** Function SetTree * Set the tree to be manipulated. * Each update of the search term will update the tree, with the most * scoring component at the top and selected. If a preselect node is set, this * is displayed. Does not take ownership of the tree. * * @param aTree that is to be modified on search updates. */ void SetTree( wxTreeCtrl* aTree ); /** Function UpdateSearchTerm * Update the search string provided by the user and narrow down the result list. * * This string is a space-separated list of terms, each of which * is applied to the components list to narrow it down. Results are scored by * relevancy (e.g. exact match scores higher than prefix-match which in turn scores * higher than substring match). This updates the search and tree on each call. * * @param aSearch is the user-provided search string. */ void UpdateSearchTerm( const wxString& aSearch ); /** Function GetSelectedComponent * * @return the selected component or NULL if there is none. */ LIB_COMPONENT* GetSelectedComponent(); private: struct TREE_NODE; static bool scoreComparator( const TREE_NODE* a1, const TREE_NODE* a2 ); std::vector<TREE_NODE*> nodes; wxString preselect_node_name; wxTreeCtrl* tree; };