KiCad PCB EDA Suite
Loading...
Searching...
No Matches
drc_re_condition_group_panel.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 (C) 2024 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 * or you may search the http://www.gnu.org website for the version 2 license,
20 * or you may write to the Free Software Foundation, Inc.,
21 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
22 */
23
26
27#include <bitmaps.h>
28
29#include <wx/sizer.h>
30#include <wx/choice.h>
31#include <wx/bmpbuttn.h>
32#include <wx/stattext.h>
33#include <wx/log.h>
34
35static constexpr const wxChar* TRACE_COND = wxT( "KI_TRACE_DRC_RULE_EDITOR" );
36
37
39 bool aTwoObjectConstraint ) :
40 wxPanel( aParent ),
41 m_board( aBoard ),
42 m_twoObjectConstraint( aTwoObjectConstraint ),
43 m_suppressCallbacks( false )
44{
45 wxLogTrace( TRACE_COND, wxS( "[DRC_RE_CONDITION_GROUP_PANEL] ctor START" ) );
46 m_mainSizer = new wxBoxSizer( wxVERTICAL );
47
48 // Label
49 wxString labelText = aTwoObjectConstraint ? _( "Where conditions match:" )
50 : _( "Where object matches:" );
51 m_labelText = new wxStaticText( this, wxID_ANY, labelText );
52 m_mainSizer->Add( m_labelText, 0, wxALL, 5 );
53
54 // Add button (added to sizer before first condition row so insert logic works)
55 m_addBtn = new wxBitmapButton( this, wxID_ANY, KiBitmapBundle( BITMAPS::small_plus ) );
56 m_addBtn->SetToolTip( _( "Add another condition" ) );
57 m_mainSizer->Add( m_addBtn, 0, wxALL | wxALIGN_RIGHT, 5 );
58
59 SetSizer( m_mainSizer );
60
61 m_addBtn->Bind( wxEVT_BUTTON, &DRC_RE_CONDITION_GROUP_PANEL::onAddClicked, this );
62
63 // Start with one empty condition row
65
66 // Ensure proper initial layout
67 m_mainSizer->Fit( this );
68 Layout();
69}
70
71
72void DRC_RE_CONDITION_GROUP_PANEL::ParseCondition( const wxString& aConditionExpr )
73{
74 wxLogTrace( TRACE_COND, wxS( "[ParseCondition] expr='%s'" ), aConditionExpr );
75
76 // Suppress change callbacks during parsing to avoid premature visibility updates
78
79 // Clear existing rows
80 while( m_conditions.size() > 1 )
81 removeConditionRow( static_cast<int>( m_conditions.size() ) - 1 );
82
83 if( aConditionExpr.IsEmpty() )
84 {
85 wxLogTrace( TRACE_COND, wxS( "[ParseCondition] empty expression, setting to ANY" ) );
86
87 if( m_conditions.empty() )
89
90 m_conditions[0].panel->ParseExpression( wxEmptyString );
91 m_suppressCallbacks = false;
92 return;
93 }
94
95 // Tokenize the expression by boolean operators
96 std::vector<std::pair<BOOL_OPERATOR, wxString>> parts;
97
98 if( !tokenizeCondition( aConditionExpr, parts ) )
99 {
100 wxLogTrace( TRACE_COND, wxS( "[ParseCondition] tokenize failed, using single custom row" ) );
101
102 // Parsing failed - fall back to single custom query row
103 if( m_conditions.empty() )
105
106 m_conditions[0].panel->ParseExpression( aConditionExpr );
107 m_suppressCallbacks = false;
108 return;
109 }
110
111 wxLogTrace( TRACE_COND, wxS( "[ParseCondition] tokenized into %zu parts" ), parts.size() );
112
113 // Create rows for each part
114 for( size_t i = 0; i < parts.size(); ++i )
115 {
116 wxLogTrace( TRACE_COND, wxS( "[ParseCondition] part[%zu] op=%d expr='%s'" ),
117 i, static_cast<int>( parts[i].first ), parts[i].second );
118
119 if( i == 0 )
120 {
121 if( m_conditions.empty() )
123
124 m_conditions[0].panel->ParseExpression( parts[i].second );
125 }
126 else
127 {
128 addConditionRow( parts[i].first );
129 m_conditions.back().panel->ParseExpression( parts[i].second );
130
131 if( m_conditions.back().operatorChoice )
132 m_conditions.back().operatorChoice->SetSelection( static_cast<int>( parts[i].first ) );
133 }
134 }
135
137
138 m_suppressCallbacks = false;
139
140 wxLogTrace( TRACE_COND, wxS( "[ParseCondition] done, HasCustomQuerySelected=%d" ),
141 HasCustomQuerySelected() ? 1 : 0 );
142}
143
144
146{
147 wxString result;
148
149 for( size_t i = 0; i < m_conditions.size(); ++i )
150 {
151 wxString rowExpr = m_conditions[i].panel->BuildExpression();
152
153 if( rowExpr.IsEmpty() )
154 continue;
155
156 if( !result.IsEmpty() )
157 {
158 switch( m_conditions[i].boolOp )
159 {
160 case BOOL_OPERATOR::AND: result += " && "; break;
161 case BOOL_OPERATOR::OR: result += " || "; break;
162 case BOOL_OPERATOR::AND_NOT: result += " && !"; break;
163 case BOOL_OPERATOR::OR_NOT: result += " || !"; break;
164 }
165 }
166
167 result += rowExpr;
168 }
169
170 return result;
171}
172
173
175{
176 for( size_t i = 0; i < m_conditions.size(); ++i )
177 {
178 bool hasCustom = m_conditions[i].panel->HasCustomQuerySelected();
179
180 wxLogTrace( TRACE_COND, wxS( "[HasCustomQuerySelected] row[%zu] type=%d hasCustom=%d" ),
181 i, static_cast<int>( m_conditions[i].panel->GetConditionType() ), hasCustom ? 1 : 0 );
182
183 if( hasCustom )
184 return true;
185 }
186
187 return false;
188}
189
190
192{
193 wxLogTrace( TRACE_COND, wxS( "[addConditionRow] START, existing rows=%zu" ), m_conditions.size() );
194
195 CONDITION_ENTRY entry;
196 entry.boolOp = aOp;
197 entry.operatorChoice = nullptr;
198 entry.rowSizer = new wxBoxSizer( wxHORIZONTAL );
199
200 // Add operator choice for non-first rows
201 if( !m_conditions.empty() )
202 {
203 wxArrayString operators;
204 operators.Add( _( "AND" ) );
205 operators.Add( _( "OR" ) );
206 operators.Add( _( "AND NOT" ) );
207 operators.Add( _( "OR NOT" ) );
208
209 entry.operatorChoice = new wxChoice( this, wxID_ANY, wxDefaultPosition, wxDefaultSize, operators );
210 entry.operatorChoice->SetSelection( static_cast<int>( aOp ) );
211 entry.rowSizer->Add( entry.operatorChoice, 0, wxALL | wxALIGN_TOP, 2 );
212
213 wxChoice* opChoice = entry.operatorChoice;
214
215 opChoice->Bind( wxEVT_CHOICE,
216 [this, opChoice]( wxCommandEvent& )
217 {
218 // Find this entry and update its operator
219 for( size_t i = 0; i < m_conditions.size(); ++i )
220 {
221 if( m_conditions[i].operatorChoice == opChoice )
222 {
223 m_conditions[i].boolOp =
224 static_cast<BOOL_OPERATOR>( opChoice->GetSelection() );
225 break;
226 }
227 }
228
231 } );
232 }
233
234 wxLogTrace( TRACE_COND, wxS( "[addConditionRow] creating row panel" ) );
236 wxLogTrace( TRACE_COND, wxS( "[addConditionRow] adding row panel to rowSizer" ) );
237 entry.rowSizer->Add( entry.panel, 1, wxEXPAND | wxALL, 0 );
238
239 // Set up callbacks
240 int rowIndex = static_cast<int>( m_conditions.size() );
241
243 [this, rowIndex]()
244 {
245 removeConditionRow( rowIndex );
246 } );
247
249 [this]()
250 {
253 } );
254
255 m_conditions.push_back( entry );
256
257 // Insert before the add button
258 m_mainSizer->Insert( m_mainSizer->GetItemCount() - 1, entry.rowSizer, 0, wxEXPAND | wxALL, 2 );
259
261 m_mainSizer->Fit( this );
262 Layout();
263
264 // Notify parent to update layout
265 if( wxWindow* parent = GetParent() )
266 parent->Layout();
267}
268
269
271{
272 if( aIndex < 0 || aIndex >= static_cast<int>( m_conditions.size() ) )
273 return;
274
275 // Don't remove the last row
276 if( m_conditions.size() <= 1 )
277 return;
278
279 CONDITION_ENTRY& entry = m_conditions[aIndex];
280
281 // Remove row sizer from main sizer and destroy widgets
282 m_mainSizer->Detach( entry.rowSizer );
283
284 if( entry.operatorChoice )
285 entry.operatorChoice->Destroy();
286
287 entry.panel->Destroy();
288 delete entry.rowSizer;
289
290 m_conditions.erase( m_conditions.begin() + aIndex );
291
292 // Update delete callbacks with new indices
293 for( size_t i = 0; i < m_conditions.size(); ++i )
294 {
295 int newIndex = static_cast<int>( i );
296
297 m_conditions[i].panel->SetDeleteCallback(
298 [this, newIndex]()
299 {
300 removeConditionRow( newIndex );
301 } );
302 }
303
305 m_mainSizer->Fit( this );
306 Layout();
307
308 // Notify parent to update layout
309 if( wxWindow* parent = GetParent() )
310 parent->Layout();
311
314}
315
316
324
325
327{
328 // Add button should always be at the end
329 m_mainSizer->Detach( m_addBtn );
330 m_mainSizer->Add( m_addBtn, 0, wxALL | wxALIGN_RIGHT, 5 );
331
332 Layout();
333}
334
335
337{
338 // Hide delete button if only one row
339 bool showDelete = m_conditions.size() > 1;
340
341 for( CONDITION_ENTRY& entry : m_conditions )
342 entry.panel->ShowDeleteButton( showDelete );
343}
344
345
347 const wxString& aExpr, std::vector<std::pair<BOOL_OPERATOR, wxString>>& aParts )
348{
349 wxString remaining = aExpr;
351 bool first = true;
352
353 while( !remaining.IsEmpty() )
354 {
355 remaining.Trim( false );
356
357 if( remaining.IsEmpty() )
358 break;
359
360 // Look for operator at start (except for first part)
361 if( !first )
362 {
363 if( remaining.StartsWith( "&&" ) )
364 {
365 remaining = remaining.Mid( 2 ).Trim( false );
366
367 if( remaining.StartsWith( "!" ) )
368 {
369 nextOp = BOOL_OPERATOR::AND_NOT;
370 remaining = remaining.Mid( 1 ).Trim( false );
371 }
372 else
373 {
374 nextOp = BOOL_OPERATOR::AND;
375 }
376 }
377 else if( remaining.StartsWith( "||" ) )
378 {
379 remaining = remaining.Mid( 2 ).Trim( false );
380
381 if( remaining.StartsWith( "!" ) )
382 {
383 nextOp = BOOL_OPERATOR::OR_NOT;
384 remaining = remaining.Mid( 1 ).Trim( false );
385 }
386 else
387 {
388 nextOp = BOOL_OPERATOR::OR;
389 }
390 }
391 else
392 {
393 // Expected operator not found
394 return false;
395 }
396 }
397
398 // Find end of this expression (next operator at depth 0)
399 int depth = 0;
400 bool inQuote = false;
401 size_t end = 0;
402
403 for( size_t i = 0; i < remaining.length(); ++i )
404 {
405 wxChar c = remaining[i];
406
407 if( c == '\\' && i + 1 < remaining.length() )
408 {
409 i++; // Skip escaped char
410 continue;
411 }
412
413 if( c == '\'' )
414 {
415 inQuote = !inQuote;
416 continue;
417 }
418
419 if( inQuote )
420 continue;
421
422 if( c == '(' )
423 depth++;
424
425 if( c == ')' )
426 depth--;
427
428 if( depth == 0 && i + 1 < remaining.length() )
429 {
430 if( remaining.Mid( i, 2 ) == "&&" || remaining.Mid( i, 2 ) == "||" )
431 {
432 end = i;
433 break;
434 }
435 }
436 }
437
438 if( end == 0 )
439 end = remaining.length();
440
441 wxString part = remaining.Left( end ).Trim( true ).Trim( false );
442 aParts.push_back( { nextOp, part } );
443
444 remaining = remaining.Mid( end );
445 first = false;
446 }
447
448 return !aParts.empty();
449}
wxBitmapBundle KiBitmapBundle(BITMAPS aBitmap, int aMinHeight)
Definition bitmap.cpp:110
Information pertinent to a Pcbnew printed circuit board.
Definition board.h:322
std::vector< CONDITION_ENTRY > m_conditions
void onAddClicked(wxCommandEvent &aEvent)
bool tokenizeCondition(const wxString &aExpr, std::vector< std::pair< BOOL_OPERATOR, wxString > > &aParts)
Tokenize a condition expression by boolean operators.
void ParseCondition(const wxString &aConditionExpr)
Parse a condition string and populate the UI.
DRC_RE_CONDITION_GROUP_PANEL(wxWindow *aParent, BOARD *aBoard, bool aTwoObjectConstraint)
void addConditionRow(BOOL_OPERATOR aOp=BOOL_OPERATOR::AND)
wxString BuildCondition() const
Build a condition string from the current UI state.
A single condition row in the condition group panel.
void SetDeleteCallback(std::function< void()> aCallback)
void SetChangeCallback(std::function< void()> aCallback)
static constexpr const wxChar * TRACE_COND
#define _(s)
DRC_RE_CONDITION_ROW_PANEL * panel
wxChoice * operatorChoice
wxBoxSizer * rowSizer
BOOL_OPERATOR boolOp
VECTOR2I end
wxString result
Test unit parsing edge cases and error handling.