KiCad PCB EDA Suite
Loading...
Searching...
No Matches
text_var_dependency.cpp
Go to the documentation of this file.
1/*
2 * This program source code file is part of KiCad, a free EDA CAD application.
3 *
4 * Copyright The KiCad Developers, see AUTHORS.txt for contributors.
5 *
6 * This program is free software; you can redistribute it and/or
7 * modify it under the terms of the GNU General Public License
8 * as published by the Free Software Foundation; either version 2
9 * of the License, or (at your option) any later version.
10 *
11 * This program is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 * GNU General Public License for more details.
15 *
16 * You should have received a copy of the GNU General Public License
17 * along with this program; if not, you may find one here:
18 * http://www.gnu.org/licenses/old-licenses/gpl-2.0.html
19 */
20
21#include <text_var_dependency.h>
22
23#include <mutex>
24#include <functional>
25#include <utility>
26
27
28namespace
29{
30// Title-block field names are defined in common/title_block.cpp and documented
31// in the Page Settings dialog. Keeping the list here (rather than pulling in
32// the title_block header) avoids a circular dependency — this module lives in
33// kicommon and title_block.cpp does too, but the field names are stable ABI.
34bool isTitleBlockField( const wxString& aName )
35{
36 return aName == wxT( "ISSUE_DATE" ) || aName == wxT( "REVISION" )
37 || aName == wxT( "TITLE" ) || aName == wxT( "COMPANY" )
38 || aName == wxT( "COMMENT1" ) || aName == wxT( "COMMENT2" )
39 || aName == wxT( "COMMENT3" ) || aName == wxT( "COMMENT4" )
40 || aName == wxT( "COMMENT5" ) || aName == wxT( "COMMENT6" )
41 || aName == wxT( "COMMENT7" ) || aName == wxT( "COMMENT8" )
42 || aName == wxT( "COMMENT9" ) || aName == wxT( "PROJECTNAME" )
43 || aName == wxT( "PAPER" ) || aName == wxT( "KICAD_VERSION" );
44}
45
46
47// Context-sensitive specials resolved by SCHEMATIC/BOARD/SCH_SHEET themselves,
48// not by the project. They cannot be driven by a listener — see TRACKER
49// routing rules.
50bool isSpecial( const wxString& aName )
51{
52 return aName == wxT( "SHEETNAME" ) || aName == wxT( "SHEETPATH" )
53 || aName == wxT( "FILENAME" ) || aName == wxT( "FILEPATH" )
54 || aName == wxT( "#" ) || aName == wxT( "##" )
55 || aName == wxT( "CURRENT_DATE" ) || aName == wxT( "LAYER" )
56 || aName == wxT( "KICAD_VERSION" );
57}
58}
59
60
62{
64
65 const int colonPos = aToken.Find( ':' );
66 const bool hasColon = colonPos != wxNOT_FOUND && colonPos > 0
67 && colonPos < static_cast<int>( aToken.length() ) - 1;
68
69 if( hasColon )
70 {
71 const wxString left = aToken.Left( colonPos );
72 const wxString right = aToken.Mid( colonPos + 1 );
73
74 // `${OP}` with `:port` suffix (SPICE operating points). Not listener-
75 // driven; kept categorized so callers can skip registration.
76 if( left == wxT( "OP" ) )
77 {
78 key.kind = KIND::OP;
79 key.primary = left;
80 key.secondary = right;
81 }
82 else
83 {
85 key.primary = left;
86 key.secondary = right;
87 }
88
89 return key;
90 }
91
92 // Bare `${OP}` (no port) also categorized as OP.
93 if( aToken == wxT( "OP" ) )
94 {
95 key.kind = KIND::OP;
96 key.primary = aToken;
97 return key;
98 }
99
100 if( isTitleBlockField( aToken ) )
101 {
103 key.primary = aToken;
104 return key;
105 }
106
107 if( isSpecial( aToken ) )
108 {
109 key.kind = KIND::SPECIAL;
110 key.primary = aToken;
111 return key;
112 }
113
114 // Default: LOCAL. The listener adapter may promote to PROJECT_VAR or
115 // ENV_VAR at registration time based on whether the token appears in
116 // PROJECT::GetTextVars() or ENV_VAR::GetPredefinedEnvVars().
117 key.kind = KIND::LOCAL;
118 key.primary = aToken;
119 return key;
120}
121
122
123std::vector<TEXT_VAR_REF_KEY> FilterTrackable( const std::vector<TEXT_VAR_REF_KEY>& aRefs )
124{
125 std::vector<TEXT_VAR_REF_KEY> out;
126 out.reserve( aRefs.size() );
127
128 for( const TEXT_VAR_REF_KEY& ref : aRefs )
129 {
130 if( ref.IsTrackable() )
131 out.push_back( ref );
132 }
133
134 return out;
135}
136
137
139{
140 std::hash<std::string> hasher;
141
142 auto mix = []( std::size_t& aSeed, std::size_t aValue )
143 {
144 // boost::hash_combine pattern — good avalanche across field boundaries.
145 aSeed ^= aValue + 0x9E3779B97F4A7C15ULL + ( aSeed << 6 ) + ( aSeed >> 2 );
146 };
147
148 // std::hash<wxString> is not universally specialized across platforms;
149 // route through utf8 std::string for portability.
150 std::size_t h = 0;
151 mix( h, static_cast<std::size_t>( aKey.kind ) );
152 mix( h, hasher( aKey.primary.ToStdString( wxConvUTF8 ) ) );
153 mix( h, hasher( aKey.secondary.ToStdString( wxConvUTF8 ) ) );
154 return h;
155}
156
157
159 const std::vector<TEXT_VAR_REF_KEY>& aKeys )
160{
161 if( !aItem )
162 return;
163
164 std::unique_lock lock( m_mutex );
165
166 if( auto it = m_itemKeys.find( aItem ); it != m_itemKeys.end() )
167 {
168 for( const TEXT_VAR_REF_KEY& oldKey : it->second )
169 {
170 if( auto depIt = m_dependents.find( oldKey ); depIt != m_dependents.end() )
171 {
172 depIt->second.erase( aItem );
173
174 if( depIt->second.empty() )
175 m_dependents.erase( depIt );
176 }
177 }
178
179 m_itemKeys.erase( it );
180 }
181
182 if( aKeys.empty() )
183 return;
184
185 for( const TEXT_VAR_REF_KEY& key : aKeys )
186 m_dependents[key].insert( aItem );
187
188 m_itemKeys[aItem] = aKeys;
189}
190
191
193{
194 if( !aItem )
195 return;
196
197 std::unique_lock lock( m_mutex );
198
199 auto it = m_itemKeys.find( aItem );
200
201 if( it == m_itemKeys.end() )
202 return;
203
204 for( const TEXT_VAR_REF_KEY& key : it->second )
205 {
206 if( auto depIt = m_dependents.find( key ); depIt != m_dependents.end() )
207 {
208 depIt->second.erase( aItem );
209
210 if( depIt->second.empty() )
211 m_dependents.erase( depIt );
212 }
213 }
214
215 m_itemKeys.erase( it );
216}
217
218
220 const std::function<void( EDA_ITEM* )>& aFn ) const
221{
222 // Snapshot under the shared lock, then release before invoking the callback.
223 // Callers in Phase 3b listener adapters will re-enter Register/Unregister
224 // during the callback (e.g. a dialog refresh that rebuilds its row list);
225 // holding the lock across arbitrary code would deadlock on the writer
226 // upgrade path.
227 std::vector<EDA_ITEM*> snapshot;
228
229 {
230 std::shared_lock lock( m_mutex );
231
232 auto it = m_dependents.find( aKey );
233
234 if( it == m_dependents.end() )
235 return;
236
237 snapshot.reserve( it->second.size() );
238 snapshot.assign( it->second.begin(), it->second.end() );
239 }
240
241 for( EDA_ITEM* item : snapshot )
242 aFn( item );
243}
244
245
247{
248 std::unique_lock lock( m_mutex );
249
250 m_dependents.clear();
251 m_itemKeys.clear();
252}
253
254
256{
257 std::shared_lock lock( m_mutex );
258
259 auto it = m_dependents.find( aKey );
260
261 return it == m_dependents.end() ? 0u : it->second.size();
262}
263
264
266{
267 std::shared_lock lock( m_mutex );
268 return m_itemKeys.size();
269}
270
271
272std::vector<TEXT_VAR_REF_KEY> TEXT_VAR_DEPENDENCY_INDEX::GetRegisteredKeys() const
273{
274 std::shared_lock lock( m_mutex );
275
276 std::vector<TEXT_VAR_REF_KEY> keys;
277 keys.reserve( m_dependents.size() );
278
279 for( const auto& [key, deps] : m_dependents )
280 keys.push_back( key );
281
282 return keys;
283}
284
285
287 m_extractSourceKeys( std::move( aSourceKeyExtractor ) )
288{
289}
290
291
293{
294 m_extractSourceKeys = std::move( aExtractor );
295}
296
297
299 InvalidateCallback aCallback )
300{
301 std::unique_lock lock( m_listenersMutex );
302 const ListenerHandle handle = m_nextListenerHandle++;
303 m_invalidateListeners.emplace( handle, std::move( aCallback ) );
304 return handle;
305}
306
307
309{
310 std::unique_lock lock( m_listenersMutex );
311 m_invalidateListeners.erase( aHandle );
312}
313
314
316 const std::vector<TEXT_VAR_REF_KEY>& aKeys )
317{
318 if( aKeys.empty() )
319 m_index.Unregister( aItem );
320 else
321 m_index.Register( aItem, aKeys );
322}
323
324
326{
327 m_index.Unregister( aItem );
328}
329
330
332{
334 return;
335
336 std::vector<TEXT_VAR_REF_KEY> sourceKeys = m_extractSourceKeys( aItem );
337
338 for( const TEXT_VAR_REF_KEY& key : sourceKeys )
339 InvalidateKey( key );
340}
341
342
344 const std::vector<TEXT_VAR_REF_KEY>& aUpdatedKeys )
345{
346 // Two responsibilities per changed item:
347 // 1. Keep its own dependency registration current — text may have been edited.
348 // 2. Fan out invalidation to items that depend on it as a cross-ref source.
349 // We do (1) before (2) so a self-referencing item sees its own refresh.
350 RegisterItem( aItem, aUpdatedKeys );
351 fanOutSourceKeys( aItem );
352}
353
354
356 std::initializer_list<TEXT_VAR_REF_KEY::KIND> aKinds )
357{
358 // Snapshot the key set once (copy is unavoidable — InvalidateKey can
359 // mutate the index through its listener callbacks, so iterating the
360 // live index under a read lock would deadlock on re-entry).
361 const std::vector<TEXT_VAR_REF_KEY> all = m_index.GetRegisteredKeys();
362
363 for( const TEXT_VAR_REF_KEY& key : all )
364 {
365 for( TEXT_VAR_REF_KEY::KIND k : aKinds )
366 {
367 if( key.kind == k )
368 {
369 InvalidateKey( key );
370 break;
371 }
372 }
373 }
374}
375
376
385
386
392
393
395{
396 // Snapshot the listener callbacks under a shared lock, then invoke
397 // unlocked. Callbacks may add/remove listeners (which would invalidate
398 // iterators) or re-enter Invalidate* paths (shared_mutex tolerates
399 // re-entrant shared locks on most implementations, but we avoid holding
400 // it during the callback anyway so writer starvation is bounded).
401 std::vector<InvalidateCallback> snapshot;
402
403 {
404 std::shared_lock lock( m_listenersMutex );
405
406 if( m_invalidateListeners.empty() )
407 return;
408
409 snapshot.reserve( m_invalidateListeners.size() );
410
411 for( const auto& [handle, cb] : m_invalidateListeners )
412 snapshot.push_back( cb );
413 }
414
415 m_index.ForEachDependent( aKey,
416 [&]( EDA_ITEM* dependent )
417 {
418 for( const InvalidateCallback& cb : snapshot )
419 cb( dependent, aKey );
420 } );
421}
A base class for most all the KiCad significant classes used in schematics and boards.
Definition eda_item.h:100
std::vector< TEXT_VAR_REF_KEY > GetRegisteredKeys() const
Enumerate every key currently present in the forward index.
std::unordered_map< TEXT_VAR_REF_KEY, std::unordered_set< EDA_ITEM * >, TEXT_VAR_REF_KEY_HASH > m_dependents
void Clear()
Drop every entry.
std::size_t DependentCount(const TEXT_VAR_REF_KEY &aKey) const
void Register(EDA_ITEM *aItem, const std::vector< TEXT_VAR_REF_KEY > &aKeys)
Replace the key set for aItem with aKeys.
std::unordered_map< EDA_ITEM *, std::vector< TEXT_VAR_REF_KEY > > m_itemKeys
void Unregister(EDA_ITEM *aItem)
Remove aItem from the index entirely.
void ForEachDependent(const TEXT_VAR_REF_KEY &aKey, const std::function< void(EDA_ITEM *)> &aFn) const
Invoke aFn exactly once per item registered under aKey.
void HandleItemChanged(EDA_ITEM *aItem, const std::vector< TEXT_VAR_REF_KEY > &aUpdatedKeys)
Handle a composite change for aItem: register its updated keys and fan out invalidation for every key...
void InvalidateVariantScoped()
Invalidate every source whose resolved value can differ between variants: CROSS_REF (footprint/symbol...
void RegisterItem(EDA_ITEM *aItem, const std::vector< TEXT_VAR_REF_KEY > &aKeys)
Register aItem in the index.
SourceKeyExtractor m_extractSourceKeys
void UnregisterItem(EDA_ITEM *aItem)
Remove aItem from the index.
std::function< void(EDA_ITEM *aDependent, const TEXT_VAR_REF_KEY &aKey)> InvalidateCallback
Called when a dependent item should recompute its resolved text.
std::unordered_map< ListenerHandle, InvalidateCallback > m_invalidateListeners
void SetSourceKeyExtractor(SourceKeyExtractor aExtractor)
ListenerHandle AddInvalidateListener(InvalidateCallback aCallback)
Register a listener that fires for every invalidation.
TEXT_VAR_DEPENDENCY_INDEX m_index
void InvalidateByKind(std::initializer_list< TEXT_VAR_REF_KEY::KIND > aKinds)
Fan out invalidation for every registered key whose KIND is one of the supplied set.
void InvalidateProjectScoped()
Invalidate every non-item source: title-block, special, project/env vars, and LOCAL (which may be a p...
std::size_t ListenerHandle
Opaque handle returned by AddInvalidateListener and consumed by RemoveInvalidateListener.
TEXT_VAR_TRACKER(SourceKeyExtractor aSourceKeyExtractor={})
ListenerHandle m_nextListenerHandle
void RemoveInvalidateListener(ListenerHandle aHandle)
Drop a previously-registered listener.
void fanOutSourceKeys(EDA_ITEM *aItem)
std::function< std::vector< TEXT_VAR_REF_KEY >(EDA_ITEM *)> SourceKeyExtractor
Return the keys aItem could source as a cross-reference target.
void InvalidateKey(const TEXT_VAR_REF_KEY &aKey)
Fan out invalidation for a single explicit key — used when a non-item source changes (e....
std::shared_mutex m_listenersMutex
STL namespace.
std::size_t operator()(const TEXT_VAR_REF_KEY &aKey) const
Identifies a single resolvable source that a text item's ${...} reference depends on.
static TEXT_VAR_REF_KEY FromToken(const wxString &aToken)
Parse a raw token (the text between ${ and }) into a key using lexical classification only — no looku...
KIND
Categorizes a reference by the source that will produce its value.
std::vector< TEXT_VAR_REF_KEY > FilterTrackable(const std::vector< TEXT_VAR_REF_KEY > &aRefs)
Filter aRefs down to the subset that should be registered in the index (drops OP and any future non-t...