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 <functional>
24#include <utility>
25
26
27namespace
28{
29// Title-block field names are defined in common/title_block.cpp and documented
30// in the Page Settings dialog. Keeping the list here (rather than pulling in
31// the title_block header) avoids a circular dependency — this module lives in
32// kicommon and title_block.cpp does too, but the field names are stable ABI.
33bool isTitleBlockField( const wxString& aName )
34{
35 return aName == wxT( "ISSUE_DATE" ) || aName == wxT( "REVISION" )
36 || aName == wxT( "TITLE" ) || aName == wxT( "COMPANY" )
37 || aName == wxT( "COMMENT1" ) || aName == wxT( "COMMENT2" )
38 || aName == wxT( "COMMENT3" ) || aName == wxT( "COMMENT4" )
39 || aName == wxT( "COMMENT5" ) || aName == wxT( "COMMENT6" )
40 || aName == wxT( "COMMENT7" ) || aName == wxT( "COMMENT8" )
41 || aName == wxT( "COMMENT9" ) || aName == wxT( "PROJECTNAME" )
42 || aName == wxT( "PAPER" ) || aName == wxT( "KICAD_VERSION" );
43}
44
45
46// Context-sensitive specials resolved by SCHEMATIC/BOARD/SCH_SHEET themselves,
47// not by the project. They cannot be driven by a listener — see TRACKER
48// routing rules.
49bool isSpecial( const wxString& aName )
50{
51 return aName == wxT( "SHEETNAME" ) || aName == wxT( "SHEETPATH" )
52 || aName == wxT( "FILENAME" ) || aName == wxT( "FILEPATH" )
53 || aName == wxT( "#" ) || aName == wxT( "##" )
54 || aName == wxT( "CURRENT_DATE" ) || aName == wxT( "LAYER" )
55 || aName == wxT( "KICAD_VERSION" );
56}
57}
58
59
61{
63
64 const int colonPos = aToken.Find( ':' );
65 const bool hasColon = colonPos != wxNOT_FOUND && colonPos > 0
66 && colonPos < static_cast<int>( aToken.length() ) - 1;
67
68 if( hasColon )
69 {
70 const wxString left = aToken.Left( colonPos );
71 const wxString right = aToken.Mid( colonPos + 1 );
72
73 // `${OP}` with `:port` suffix (SPICE operating points). Not listener-
74 // driven; kept categorized so callers can skip registration.
75 if( left == wxT( "OP" ) )
76 {
77 key.kind = KIND::OP;
78 key.primary = left;
79 key.secondary = right;
80 }
81 else
82 {
84 key.primary = left;
85 key.secondary = right;
86 }
87
88 return key;
89 }
90
91 // Bare `${OP}` (no port) also categorized as OP.
92 if( aToken == wxT( "OP" ) )
93 {
94 key.kind = KIND::OP;
95 key.primary = aToken;
96 return key;
97 }
98
99 if( isTitleBlockField( aToken ) )
100 {
102 key.primary = aToken;
103 return key;
104 }
105
106 if( isSpecial( aToken ) )
107 {
108 key.kind = KIND::SPECIAL;
109 key.primary = aToken;
110 return key;
111 }
112
113 // Default: LOCAL. The listener adapter may promote to PROJECT_VAR or
114 // ENV_VAR at registration time based on whether the token appears in
115 // PROJECT::GetTextVars() or ENV_VAR::GetPredefinedEnvVars().
116 key.kind = KIND::LOCAL;
117 key.primary = aToken;
118 return key;
119}
120
121
122std::vector<TEXT_VAR_REF_KEY> FilterTrackable( const std::vector<TEXT_VAR_REF_KEY>& aRefs )
123{
124 std::vector<TEXT_VAR_REF_KEY> out;
125 out.reserve( aRefs.size() );
126
127 for( const TEXT_VAR_REF_KEY& ref : aRefs )
128 {
129 if( ref.IsTrackable() )
130 out.push_back( ref );
131 }
132
133 return out;
134}
135
136
138{
139 std::hash<std::string> hasher;
140
141 auto mix = []( std::size_t& aSeed, std::size_t aValue )
142 {
143 // boost::hash_combine pattern — good avalanche across field boundaries.
144 aSeed ^= aValue + 0x9E3779B97F4A7C15ULL + ( aSeed << 6 ) + ( aSeed >> 2 );
145 };
146
147 // std::hash<wxString> is not universally specialized across platforms;
148 // route through utf8 std::string for portability.
149 std::size_t h = 0;
150 mix( h, static_cast<std::size_t>( aKey.kind ) );
151 mix( h, hasher( aKey.primary.ToStdString( wxConvUTF8 ) ) );
152 mix( h, hasher( aKey.secondary.ToStdString( wxConvUTF8 ) ) );
153 return h;
154}
155
156
158 const std::vector<TEXT_VAR_REF_KEY>& aKeys )
159{
160 if( !aItem )
161 return;
162
163 std::unique_lock lock( m_mutex );
164
165 if( auto it = m_itemKeys.find( aItem ); it != m_itemKeys.end() )
166 {
167 for( const TEXT_VAR_REF_KEY& oldKey : it->second )
168 {
169 if( auto depIt = m_dependents.find( oldKey ); depIt != m_dependents.end() )
170 {
171 depIt->second.erase( aItem );
172
173 if( depIt->second.empty() )
174 m_dependents.erase( depIt );
175 }
176 }
177
178 m_itemKeys.erase( it );
179 }
180
181 if( aKeys.empty() )
182 return;
183
184 for( const TEXT_VAR_REF_KEY& key : aKeys )
185 m_dependents[key].insert( aItem );
186
187 m_itemKeys[aItem] = aKeys;
188}
189
190
192{
193 if( !aItem )
194 return;
195
196 std::unique_lock lock( m_mutex );
197
198 auto it = m_itemKeys.find( aItem );
199
200 if( it == m_itemKeys.end() )
201 return;
202
203 for( const TEXT_VAR_REF_KEY& key : it->second )
204 {
205 if( auto depIt = m_dependents.find( key ); depIt != m_dependents.end() )
206 {
207 depIt->second.erase( aItem );
208
209 if( depIt->second.empty() )
210 m_dependents.erase( depIt );
211 }
212 }
213
214 m_itemKeys.erase( it );
215}
216
217
219 const std::function<void( EDA_ITEM* )>& aFn ) const
220{
221 // Snapshot under the shared lock, then release before invoking the callback.
222 // Callers in Phase 3b listener adapters will re-enter Register/Unregister
223 // during the callback (e.g. a dialog refresh that rebuilds its row list);
224 // holding the lock across arbitrary code would deadlock on the writer
225 // upgrade path.
226 std::vector<EDA_ITEM*> snapshot;
227
228 {
229 std::shared_lock lock( m_mutex );
230
231 auto it = m_dependents.find( aKey );
232
233 if( it == m_dependents.end() )
234 return;
235
236 snapshot.reserve( it->second.size() );
237 snapshot.assign( it->second.begin(), it->second.end() );
238 }
239
240 for( EDA_ITEM* item : snapshot )
241 aFn( item );
242}
243
244
246{
247 std::unique_lock lock( m_mutex );
248
249 m_dependents.clear();
250 m_itemKeys.clear();
251}
252
253
255{
256 std::shared_lock lock( m_mutex );
257
258 auto it = m_dependents.find( aKey );
259
260 return it == m_dependents.end() ? 0u : it->second.size();
261}
262
263
265{
266 std::shared_lock lock( m_mutex );
267 return m_itemKeys.size();
268}
269
270
271std::vector<TEXT_VAR_REF_KEY> TEXT_VAR_DEPENDENCY_INDEX::GetRegisteredKeys() const
272{
273 std::shared_lock lock( m_mutex );
274
275 std::vector<TEXT_VAR_REF_KEY> keys;
276 keys.reserve( m_dependents.size() );
277
278 for( const auto& [key, deps] : m_dependents )
279 keys.push_back( key );
280
281 return keys;
282}
283
284
286 m_extractSourceKeys( std::move( aSourceKeyExtractor ) )
287{
288}
289
290
292{
293 m_extractSourceKeys = std::move( aExtractor );
294}
295
296
298 InvalidateCallback aCallback )
299{
300 std::unique_lock lock( m_listenersMutex );
301 const ListenerHandle handle = m_nextListenerHandle++;
302 m_invalidateListeners.emplace( handle, std::move( aCallback ) );
303 return handle;
304}
305
306
308{
309 std::unique_lock lock( m_listenersMutex );
310 m_invalidateListeners.erase( aHandle );
311}
312
313
315 const std::vector<TEXT_VAR_REF_KEY>& aKeys )
316{
317 if( aKeys.empty() )
318 m_index.Unregister( aItem );
319 else
320 m_index.Register( aItem, aKeys );
321}
322
323
325{
326 m_index.Unregister( aItem );
327}
328
329
331{
333 return;
334
335 std::vector<TEXT_VAR_REF_KEY> sourceKeys = m_extractSourceKeys( aItem );
336
337 for( const TEXT_VAR_REF_KEY& key : sourceKeys )
338 InvalidateKey( key );
339}
340
341
343 const std::vector<TEXT_VAR_REF_KEY>& aUpdatedKeys )
344{
345 // Two responsibilities per changed item:
346 // 1. Keep its own dependency registration current — text may have been edited.
347 // 2. Fan out invalidation to items that depend on it as a cross-ref source.
348 // We do (1) before (2) so a self-referencing item sees its own refresh.
349 RegisterItem( aItem, aUpdatedKeys );
350 fanOutSourceKeys( aItem );
351}
352
353
355 std::initializer_list<TEXT_VAR_REF_KEY::KIND> aKinds )
356{
357 // Snapshot the key set once (copy is unavoidable — InvalidateKey can
358 // mutate the index through its listener callbacks, so iterating the
359 // live index under a read lock would deadlock on re-entry).
360 const std::vector<TEXT_VAR_REF_KEY> all = m_index.GetRegisteredKeys();
361
362 for( const TEXT_VAR_REF_KEY& key : all )
363 {
364 for( TEXT_VAR_REF_KEY::KIND k : aKinds )
365 {
366 if( key.kind == k )
367 {
368 InvalidateKey( key );
369 break;
370 }
371 }
372 }
373}
374
375
384
385
391
392
394{
395 // Snapshot the listener callbacks under a shared lock, then invoke
396 // unlocked. Callbacks may add/remove listeners (which would invalidate
397 // iterators) or re-enter Invalidate* paths (shared_mutex tolerates
398 // re-entrant shared locks on most implementations, but we avoid holding
399 // it during the callback anyway so writer starvation is bounded).
400 std::vector<InvalidateCallback> snapshot;
401
402 {
403 std::shared_lock lock( m_listenersMutex );
404
405 if( m_invalidateListeners.empty() )
406 return;
407
408 snapshot.reserve( m_invalidateListeners.size() );
409
410 for( const auto& [handle, cb] : m_invalidateListeners )
411 snapshot.push_back( cb );
412 }
413
414 m_index.ForEachDependent( aKey,
415 [&]( EDA_ITEM* dependent )
416 {
417 for( const InvalidateCallback& cb : snapshot )
418 cb( dependent, aKey );
419 } );
420}
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...