summaryrefslogtreecommitdiff
path: root/eeschema/component_tree_search_container.cpp
blob: c3061f95e9fae4faa947eb0211bd2d389fe5035e (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
/* -*- 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 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
{
    // 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()
{
    BOOST_FOREACH( 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.GetEntryNames( 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 );

    BOOST_FOREACH( const wxString& aName, aAliasNameList )
    {
        LIB_ALIAS* a;

        if( aOptionalLib )
            a = aOptionalLib->FindAlias( aName );
        else
            a = m_libs->FindLibraryEntry( 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();

    BOOST_FOREACH( 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;
}


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.
    BOOST_FOREACH( 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();

        BOOST_FOREACH( 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;

            if( term == node->MatchName )
                node->MatchScore += 1000;  // exact match. High score :)
            else if( (found_pos = node->MatchName.Find( term ) ) != wxNOT_FOUND )
            {
                // Substring match. The earlier in the string the better.  score += 20..40
                node->MatchScore += matchPosScore( found_pos, 20 ) + 20;
            }
            else if( node->Parent->MatchName.Find( term ) != wxNOT_FOUND )
                node->MatchScore += 19;   // parent name matches.         score += 19
            else if( ( found_pos = node->SearchText.Find( term ) ) != wxNOT_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.
        }
    }

    // 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;

    BOOST_FOREACH( 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;

    BOOST_FOREACH( 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 we are a nicely scored alias, we want to have it visible. Also, if there
        // is only a single library in this container, we want to have it unfolded
        // (example: power library).
        if( node->Type == TREE_NODE::TYPE_ALIAS
             && ( node->MatchScore > kLowestDefaultScore || m_libraries_added == 1 ) )
        {
            m_tree->Expand( node->TreeId );

            if( first_match == NULL )
                first_match = node;   // First, highest scoring: the "I am feeling lucky" element.
        }

        // 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 )                      // Highest score search match pre-selected.
    {
        m_tree->SelectItem( first_match->TreeId );
        m_tree->EnsureVisible( first_match->TreeId );
    }
    else if( preselected_node )            // No search, so history item preselected.
    {
        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
}