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