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 STD_BITMAP_BUTTON( this, wxID_ANY, wxNullBitmap );
56 m_addBtn->SetToolTip( _( "Add another condition" ) );
57 m_mainSizer->Add( m_addBtn, 0, wxALL | wxALIGN_RIGHT, 5 );
58
59 SetSizer( m_mainSizer );
60
62 m_addBtn->Bind( wxEVT_BUTTON, &DRC_RE_CONDITION_GROUP_PANEL::onAddClicked, this );
63
64 // Start with one empty condition row
66
67 // Ensure proper initial layout
68 m_mainSizer->Fit( this );
69 Layout();
70}
71
72
73void DRC_RE_CONDITION_GROUP_PANEL::ParseCondition( const wxString& aConditionExpr )
74{
75 wxLogTrace( TRACE_COND, wxS( "[ParseCondition] expr='%s'" ), aConditionExpr );
76
77 // Suppress change callbacks during parsing to avoid premature visibility updates
79
80 // Clear existing rows
81 while( m_conditions.size() > 1 )
82 removeConditionRow( static_cast<int>( m_conditions.size() ) - 1 );
83
84 if( aConditionExpr.IsEmpty() )
85 {
86 wxLogTrace( TRACE_COND, wxS( "[ParseCondition] empty expression, setting to ANY" ) );
87
88 if( m_conditions.empty() )
90
91 m_conditions[0].panel->ParseExpression( wxEmptyString );
92 m_suppressCallbacks = false;
93 return;
94 }
95
96 // Tokenize the expression by boolean operators
97 std::vector<std::pair<BOOL_OPERATOR, wxString>> parts;
98
99 if( !tokenizeCondition( aConditionExpr, parts ) )
100 {
101 wxLogTrace( TRACE_COND, wxS( "[ParseCondition] tokenize failed, using single custom row" ) );
102
103 // Parsing failed - fall back to single custom query row
104 if( m_conditions.empty() )
106
107 m_conditions[0].panel->ParseExpression( aConditionExpr );
108 m_suppressCallbacks = false;
109 return;
110 }
111
112 wxLogTrace( TRACE_COND, wxS( "[ParseCondition] tokenized into %zu parts" ), parts.size() );
113
114 // Create rows for each part
115 for( size_t i = 0; i < parts.size(); ++i )
116 {
117 wxLogTrace( TRACE_COND, wxS( "[ParseCondition] part[%zu] op=%d expr='%s'" ),
118 i, static_cast<int>( parts[i].first ), parts[i].second );
119
120 if( i == 0 )
121 {
122 if( m_conditions.empty() )
124
125 m_conditions[0].panel->ParseExpression( parts[i].second );
126 }
127 else
128 {
129 addConditionRow( parts[i].first );
130 m_conditions.back().panel->ParseExpression( parts[i].second );
131
132 if( m_conditions.back().operatorChoice )
133 m_conditions.back().operatorChoice->SetSelection( static_cast<int>( parts[i].first ) );
134 }
135 }
136
138
139 m_suppressCallbacks = false;
140
141 wxLogTrace( TRACE_COND, wxS( "[ParseCondition] done, HasCustomQuerySelected=%d" ),
142 HasCustomQuerySelected() ? 1 : 0 );
143}
144
145
147{
148 wxString result;
149
150 for( size_t i = 0; i < m_conditions.size(); ++i )
151 {
152 wxString rowExpr = m_conditions[i].panel->BuildExpression();
153
154 if( rowExpr.IsEmpty() )
155 continue;
156
157 if( !result.IsEmpty() )
158 {
159 switch( m_conditions[i].boolOp )
160 {
161 case BOOL_OPERATOR::AND: result += " && "; break;
162 case BOOL_OPERATOR::OR: result += " || "; break;
163 case BOOL_OPERATOR::AND_NOT: result += " && !"; break;
164 case BOOL_OPERATOR::OR_NOT: result += " || !"; break;
165 }
166 }
167
168 result += rowExpr;
169 }
170
171 return result;
172}
173
174
176{
177 for( size_t i = 0; i < m_conditions.size(); ++i )
178 {
179 bool hasCustom = m_conditions[i].panel->HasCustomQuerySelected();
180
181 wxLogTrace( TRACE_COND, wxS( "[HasCustomQuerySelected] row[%zu] type=%d hasCustom=%d" ),
182 i, static_cast<int>( m_conditions[i].panel->GetConditionType() ), hasCustom ? 1 : 0 );
183
184 if( hasCustom )
185 return true;
186 }
187
188 return false;
189}
190
191
193{
194 wxLogTrace( TRACE_COND, wxS( "[addConditionRow] START, existing rows=%zu" ), m_conditions.size() );
195
196 CONDITION_ENTRY entry;
197 entry.boolOp = aOp;
198 entry.operatorChoice = nullptr;
199 entry.rowSizer = new wxBoxSizer( wxHORIZONTAL );
200
201 // Add operator choice for non-first rows
202 if( !m_conditions.empty() )
203 {
204 wxArrayString operators;
205 operators.Add( _( "AND" ) );
206 operators.Add( _( "OR" ) );
207 operators.Add( _( "AND NOT" ) );
208 operators.Add( _( "OR NOT" ) );
209
210 entry.operatorChoice = new wxChoice( this, wxID_ANY, wxDefaultPosition, wxDefaultSize, operators );
211 entry.operatorChoice->SetSelection( static_cast<int>( aOp ) );
212 entry.rowSizer->Add( entry.operatorChoice, 0, wxALL | wxALIGN_TOP, 2 );
213
214 wxChoice* opChoice = entry.operatorChoice;
215
216 opChoice->Bind( wxEVT_CHOICE,
217 [this, opChoice]( wxCommandEvent& )
218 {
219 // Find this entry and update its operator
220 for( size_t i = 0; i < m_conditions.size(); ++i )
221 {
222 if( m_conditions[i].operatorChoice == opChoice )
223 {
224 m_conditions[i].boolOp =
225 static_cast<BOOL_OPERATOR>( opChoice->GetSelection() );
226 break;
227 }
228 }
229
232 } );
233 }
234
235 wxLogTrace( TRACE_COND, wxS( "[addConditionRow] creating row panel" ) );
237 wxLogTrace( TRACE_COND, wxS( "[addConditionRow] adding row panel to rowSizer" ) );
238 entry.rowSizer->Add( entry.panel, 1, wxEXPAND | wxALL, 0 );
239
240 // Set up callbacks
241 int rowIndex = static_cast<int>( m_conditions.size() );
242
244 [this, rowIndex]()
245 {
246 removeConditionRow( rowIndex );
247 } );
248
250 [this]()
251 {
254 } );
255
256 m_conditions.push_back( entry );
257
258 // Insert before the add button
259 m_mainSizer->Insert( m_mainSizer->GetItemCount() - 1, entry.rowSizer, 0, wxEXPAND | wxALL, 2 );
260
262 m_mainSizer->Fit( this );
263 Layout();
264
265 // Notify parent to update layout
266 if( wxWindow* parent = GetParent() )
267 parent->Layout();
268}
269
270
272{
273 if( aIndex < 0 || aIndex >= static_cast<int>( m_conditions.size() ) )
274 return;
275
276 // Don't remove the last row
277 if( m_conditions.size() <= 1 )
278 return;
279
280 CONDITION_ENTRY& entry = m_conditions[aIndex];
281
282 // Remove row sizer from main sizer and destroy widgets
283 m_mainSizer->Detach( entry.rowSizer );
284
285 if( entry.operatorChoice )
286 entry.operatorChoice->Destroy();
287
288 entry.panel->Destroy();
289 delete entry.rowSizer;
290
291 m_conditions.erase( m_conditions.begin() + aIndex );
292
293 // Update delete callbacks with new indices
294 for( size_t i = 0; i < m_conditions.size(); ++i )
295 {
296 int newIndex = static_cast<int>( i );
297
298 m_conditions[i].panel->SetDeleteCallback(
299 [this, newIndex]()
300 {
301 removeConditionRow( newIndex );
302 } );
303 }
304
306 m_mainSizer->Fit( this );
307 Layout();
308
309 // Notify parent to update layout
310 if( wxWindow* parent = GetParent() )
311 parent->Layout();
312
315}
316
317
325
326
328{
329 // Add button should always be at the end
330 m_mainSizer->Detach( m_addBtn );
331 m_mainSizer->Add( m_addBtn, 0, wxALL | wxALIGN_RIGHT, 5 );
332
333 Layout();
334}
335
336
338{
339 // Hide delete button if only one row
340 bool showDelete = m_conditions.size() > 1;
341
342 for( CONDITION_ENTRY& entry : m_conditions )
343 entry.panel->ShowDeleteButton( showDelete );
344}
345
346
348 const wxString& aExpr, std::vector<std::pair<BOOL_OPERATOR, wxString>>& aParts )
349{
350 wxString remaining = aExpr;
352 bool first = true;
353
354 while( !remaining.IsEmpty() )
355 {
356 remaining.Trim( false );
357
358 if( remaining.IsEmpty() )
359 break;
360
361 // Look for operator at start (except for first part)
362 if( !first )
363 {
364 if( remaining.StartsWith( "&&" ) )
365 {
366 remaining = remaining.Mid( 2 ).Trim( false );
367
368 if( remaining.StartsWith( "!" ) )
369 {
370 nextOp = BOOL_OPERATOR::AND_NOT;
371 remaining = remaining.Mid( 1 ).Trim( false );
372 }
373 else
374 {
375 nextOp = BOOL_OPERATOR::AND;
376 }
377 }
378 else if( remaining.StartsWith( "||" ) )
379 {
380 remaining = remaining.Mid( 2 ).Trim( false );
381
382 if( remaining.StartsWith( "!" ) )
383 {
384 nextOp = BOOL_OPERATOR::OR_NOT;
385 remaining = remaining.Mid( 1 ).Trim( false );
386 }
387 else
388 {
389 nextOp = BOOL_OPERATOR::OR;
390 }
391 }
392 else
393 {
394 // Expected operator not found
395 return false;
396 }
397 }
398
399 // Find end of this expression (next operator at depth 0)
400 int depth = 0;
401 bool inQuote = false;
402 size_t end = 0;
403
404 for( size_t i = 0; i < remaining.length(); ++i )
405 {
406 wxChar c = remaining[i];
407
408 if( c == '\\' && i + 1 < remaining.length() )
409 {
410 i++; // Skip escaped char
411 continue;
412 }
413
414 if( c == '\'' )
415 {
416 inQuote = !inQuote;
417 continue;
418 }
419
420 if( inQuote )
421 continue;
422
423 if( c == '(' )
424 depth++;
425
426 if( c == ')' )
427 depth--;
428
429 if( depth == 0 && i + 1 < remaining.length() )
430 {
431 if( remaining.Mid( i, 2 ) == "&&" || remaining.Mid( i, 2 ) == "||" )
432 {
433 end = i;
434 break;
435 }
436 }
437 }
438
439 if( end == 0 )
440 end = remaining.length();
441
442 wxString part = remaining.Left( end ).Trim( true ).Trim( false );
443 aParts.push_back( { nextOp, part } );
444
445 remaining = remaining.Mid( end );
446 first = false;
447 }
448
449 return !aParts.empty();
450}
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)
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.