KiCad PCB EDA Suite
panel_setup_rules.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) 2020-2021 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 
24 #include <bitmaps.h>
25 #include <confirm.h>
26 #include <widgets/paged_dialog.h>
27 #include <pcb_edit_frame.h>
28 #include <pcb_expr_evaluator.h>
29 #include <board.h>
30 #include <board_design_settings.h>
31 #include <project.h>
32 #include <string_utils.h>
33 #include <tool/tool_manager.h>
34 #include <panel_setup_rules.h>
35 #include <wx_html_report_box.h>
36 #include <wx/treebook.h>
38 #include <scintilla_tricks.h>
39 #include <drc/drc_rule_parser.h>
40 #include <tools/drc_tool.h>
41 
43  PANEL_SETUP_RULES_BASE( aParent->GetTreebook() ),
44  m_Parent( aParent ),
45  m_frame( aFrame ),
46  m_scintillaTricks( nullptr ),
47  m_helpWindow( nullptr )
48 {
49  m_scintillaTricks = new SCINTILLA_TRICKS( m_textEditor, wxT( "()" ), false,
50  [this]()
51  {
52  wxPostEvent( m_Parent, wxCommandEvent( wxEVT_COMMAND_BUTTON_CLICKED, wxID_OK ) );
53  } );
54 
55  m_textEditor->AutoCompSetSeparator( '|' );
56 
57  m_netClassRegex.Compile( "NetClass\\s*[!=]=\\s*$", wxRE_ADVANCED );
58  m_netNameRegex.Compile( "NetName\\s*[!=]=\\s*$", wxRE_ADVANCED );
59  m_typeRegex.Compile( "Type\\s*[!=]=\\s*$", wxRE_ADVANCED );
60  m_padTypeRegex.Compile( "Pad_Type\\s*[!=]=\\s*$", wxRE_ADVANCED );
61  m_fabPropRegex.Compile( "Fabrication_Property\\s*[!=]=\\s*$", wxRE_ADVANCED );
62 
63  m_compileButton->SetBitmap( KiBitmap( BITMAPS::drc ) );
64 
65  m_textEditor->Bind( wxEVT_STC_CHARADDED, &PANEL_SETUP_RULES::onScintillaCharAdded, this );
66  m_textEditor->Bind( wxEVT_STC_AUTOCOMP_CHAR_DELETED, &PANEL_SETUP_RULES::onScintillaCharAdded, this );
67  m_textEditor->Bind( wxEVT_CHAR_HOOK, &PANEL_SETUP_RULES::onCharHook, this );
68 }
69 
70 
72 {
73  delete m_scintillaTricks;
74 
75  if( m_helpWindow )
76  m_helpWindow->Destroy();
77 };
78 
79 
80 void PANEL_SETUP_RULES::onCharHook( wxKeyEvent& aEvent )
81 {
82  if( aEvent.GetKeyCode() == WXK_ESCAPE && !m_textEditor->AutoCompActive() )
83  {
84  if( m_originalText != m_textEditor->GetText() )
85  {
86  if( !IsOK( this, _( "Cancel Changes?" ) ) )
87  return;
88  }
89  }
90 
91  aEvent.Skip();
92 }
93 
94 
95 void PANEL_SETUP_RULES::onScintillaCharAdded( wxStyledTextEvent &aEvent )
96 {
98  m_textEditor->SearchAnchor();
99 
100  wxString rules = m_textEditor->GetText();
101  int currentPos = m_textEditor->GetCurrentPos();
102  int startPos = 0;
103 
104  for( int line = m_textEditor->LineFromPosition( currentPos ); line > 0; line-- )
105  {
106  int lineStart = m_textEditor->PositionFromLine( line );
107  wxString beginning = m_textEditor->GetTextRange( lineStart, lineStart + 10 );
108 
109  if( beginning.StartsWith( "(rule " ) )
110  {
111  startPos = lineStart;
112  break;
113  }
114  }
115 
116  enum
117  {
118  NONE,
119  STRING,
120  SEXPR_OPEN,
121  SEXPR_TOKEN,
122  SEXPR_STRING,
123  STRUCT_REF
124  };
125 
126  auto isDisallowToken =
127  []( const wxString& token ) -> bool
128  {
129  return token == "buried_via"
130  || token == "graphic"
131  || token == "hole"
132  || token == "micro_via"
133  || token == "pad"
134  || token == "text"
135  || token == "track"
136  || token == "via"
137  || token == "zone";
138  };
139 
140  std::stack<wxString> sexprs;
141  wxString partial;
142  wxString last;
143  int context = NONE;
144  int expr_context = NONE;
145 
146  for( int i = startPos; i < currentPos; ++i )
147  {
148  wxChar c = m_textEditor->GetCharAt( i );
149 
150  if( c == '\\' )
151  {
152  i++; // skip escaped char
153  }
154  else if( context == STRING )
155  {
156  if( c == '"' )
157  {
158  context = NONE;
159  }
160  else
161  {
162  if( expr_context == STRING )
163  {
164  if( c == '\'' )
165  expr_context = NONE;
166  else
167  partial += c;
168  }
169  else if( c == '\'' )
170  {
171  last = partial;
172  partial = wxEmptyString;
173  expr_context = STRING;
174  }
175  else if( c == '.' )
176  {
177  partial = wxEmptyString;
178  expr_context = STRUCT_REF;
179  }
180  else
181  {
182  partial += c;
183  }
184  }
185  }
186  else if( c == '"' )
187  {
188  last = partial;
189  partial = wxEmptyString;
190  context = STRING;
191  }
192  else if( c == '(' )
193  {
194  if( context == SEXPR_OPEN && !partial.IsEmpty() )
195  {
196  m_textEditor->AutoCompCancel();
197  sexprs.push( partial );
198  }
199 
200  partial = wxEmptyString;
201  context = SEXPR_OPEN;
202  }
203  else if( c == ')' )
204  {
205  while( !sexprs.empty() && ( sexprs.top() == "assertion"
206  || sexprs.top() == "disallow"
207  || isDisallowToken( sexprs.top() )
208  || sexprs.top() == "min_resolved_spokes"
209  || sexprs.top() == "zone_connection" ) )
210  {
211  sexprs.pop();
212  }
213 
214  if( !sexprs.empty() )
215  sexprs.pop();
216 
217  context = NONE;
218  }
219  else if( c == ' ' )
220  {
221  if( context == SEXPR_OPEN && ( partial == "constraint"
222  || partial == "disallow"
223  || partial == "layer"
224  || partial == "severity" ) )
225  {
226  m_textEditor->AutoCompCancel();
227  sexprs.push( partial );
228 
229  partial = wxEmptyString;
230  context = SEXPR_TOKEN;
231  continue;
232  }
233  else if( partial == "disallow"
234  || isDisallowToken( partial )
235  || partial == "min_resolved_spokes"
236  || partial == "zone_connection" )
237  {
238  m_textEditor->AutoCompCancel();
239  sexprs.push( partial );
240 
241  partial = wxEmptyString;
242  context = SEXPR_TOKEN;
243  continue;
244  }
245  else if( partial == "rule"
246  || partial == "assertion"
247  || partial == "condition" )
248  {
249  m_textEditor->AutoCompCancel();
250  sexprs.push( partial );
251 
252  partial = wxEmptyString;
253  context = SEXPR_STRING;
254  continue;
255  }
256 
257  context = NONE;
258  }
259  else
260  {
261  partial += c;
262  }
263  }
264 
265  wxString tokens;
266 
267  if( context == SEXPR_OPEN )
268  {
269  if( sexprs.empty() )
270  {
271  tokens = "rule|"
272  "version";
273  }
274  else if( sexprs.top() == "rule" )
275  {
276  tokens = "condition|"
277  "constraint|"
278  "layer";
279  }
280  else if( sexprs.top() == "constraint" )
281  {
282  tokens = "max|min|opt";
283  }
284  }
285  else if( context == SEXPR_TOKEN )
286  {
287  if( sexprs.empty() )
288  {
289  /* badly formed grammar */
290  }
291  else if( sexprs.top() == "constraint" )
292  {
293  tokens = "annular_width|"
294  "clearance|"
295  "courtyard_clearance|"
296  "diff_pair_gap|"
297  "diff_pair_uncoupled|"
298  "disallow|"
299  "edge_clearance|"
300  "length|"
301  "hole_clearance|"
302  "hole_size|"
303  "hole_to_hole|"
304  "silk_clearance|"
305  "skew|"
306  "track_width|"
307  "via_count|"
308  "via_diameter";
309  }
310  else if( sexprs.top() == "disallow" || isDisallowToken( sexprs.top() ) )
311  {
312  tokens = "buried_via|"
313  "graphic|"
314  "hole|"
315  "micro_via|"
316  "pad|"
317  "text|"
318  "track|"
319  "via|"
320  "zone";
321  }
322  else if( sexprs.top() == "layer" )
323  {
324  tokens = "inner|outer|\"x\"";
325  }
326  else if( sexprs.top() == "severity" )
327  {
328  tokens = "warning|error|ignore|exclusion";
329  }
330  }
331  else if( context == STRING && !sexprs.empty() && sexprs.top() == "condition" )
332  {
333  if( expr_context == STRUCT_REF )
334  {
336  std::set<wxString> propNames;
337 
338  for( const PROPERTY_MANAGER::CLASS_INFO& cls : propMgr.GetAllClasses() )
339  {
340  const PROPERTY_LIST& props = propMgr.GetProperties( cls.type );
341 
342  for( PROPERTY_BASE* prop : props )
343  {
344  wxString ref( prop->Name() );
345  ref.Replace( " ", "_" );
346  propNames.insert( ref );
347  }
348  }
349 
350  for( const wxString& propName : propNames )
351  tokens += "|" + propName;
352 
354 
355  for( const wxString& funcSig : functions.GetSignatures() )
356  tokens += "|" + funcSig;
357  }
358  else if( expr_context == STRING )
359  {
360  if( m_netClassRegex.Matches( last ) )
361  {
362  BOARD* board = m_frame->GetBoard();
364 
365  for( const std::pair<const wxString, NETCLASSPTR>& entry : bds.GetNetClasses() )
366  tokens += "|" + entry.first;
367  }
368  else if( m_netNameRegex.Matches( last ) )
369  {
370  BOARD* board = m_frame->GetBoard();
371 
372  for( const wxString& netnameCandidate : board->GetNetClassAssignmentCandidates() )
373  tokens += "|" + netnameCandidate;
374  }
375  else if( m_typeRegex.Matches( last ) )
376  {
377  tokens = "Dimension|"
378  "Footprint|"
379  "Graphic|"
380  "Group|"
381  "Leader|"
382  "Pad|"
383  "Target|"
384  "Text|"
385  "Track|"
386  "Via|"
387  "Zone";
388  }
389  else if( m_padTypeRegex.Matches( last ) )
390  {
391  tokens = "Through-hole|"
392  "SMD|"
393  "Edge connector|"
394  "NPTH, mechanical";
395  }
396  else if( m_fabPropRegex.Matches( last ) )
397  {
398  tokens = "None|"
399  "BGA pad|"
400  "Fiducial, global to board|"
401  "Fiducial, local to footprint|"
402  "Test point pad|"
403  "Heatsink pad|"
404  "Castellated pad";
405  }
406  }
407  }
408 
409  if( !tokens.IsEmpty() )
410  m_scintillaTricks->DoAutocomplete( partial, wxSplit( tokens, '|' ) );
411 }
412 
413 
414 void PANEL_SETUP_RULES::OnCompile( wxCommandEvent& event )
415 {
417 
418  try
419  {
420  std::vector<DRC_RULE*> dummyRules;
421 
422  DRC_RULES_PARSER parser( m_textEditor->GetText(), _( "DRC rules" ) );
423 
424  parser.Parse( dummyRules, m_errorsReport );
425  }
426  catch( PARSE_ERROR& pe )
427  {
428  wxString msg = wxString::Format( "%s <a href='%d:%d'>%s</a>%s",
429  _( "ERROR:" ),
430  pe.lineNumber,
431  pe.byteIndex,
432  pe.ParseProblem(),
433  wxEmptyString );
434 
436  }
437 
439 }
440 
441 
442 void PANEL_SETUP_RULES::OnErrorLinkClicked( wxHtmlLinkEvent& event )
443 {
444  wxString link = event.GetLinkInfo().GetHref();
445  wxArrayString parts;
446  long line = 0, offset = 0;
447 
448  wxStringSplit( link, parts, ':' );
449 
450  if( parts.size() > 1 )
451  {
452  parts[0].ToLong( &line );
453  parts[1].ToLong( &offset );
454  }
455 
456  int pos = m_textEditor->PositionFromLine( line - 1 ) + ( offset - 1 );
457 
458  m_textEditor->GotoPos( pos );
459 
460  m_textEditor->SetFocus();
461 }
462 
463 
465 {
466  wxFileName rulesFile( m_frame->GetDesignRulesPath() );
467 
468  if( rulesFile.FileExists() )
469  {
470  wxTextFile file( rulesFile.GetFullPath() );
471 
472  if( file.Open() )
473  {
474  for ( wxString str = file.GetFirstLine(); !file.Eof(); str = file.GetNextLine() )
475  {
477  m_textEditor->AddText( str << '\n' );
478  }
479 
480  wxCommandEvent dummy;
481  OnCompile( dummy );
482  }
483  }
484 
485  m_originalText = m_textEditor->GetText();
486 
487  if( m_frame->Prj().IsNullProject() )
488  {
489  m_textEditor->ClearAll();
490  m_textEditor->AddText( _( "Design rules cannot be added without a project" ) );
491  m_textEditor->Disable();
492  }
493 
494  return true;
495 }
496 
497 
499 {
500  if( m_originalText == m_textEditor->GetText() )
501  return true;
502 
503  if( m_frame->Prj().IsNullProject() )
504  return true;
505 
506  wxString rulesFilepath = m_frame->GetDesignRulesPath();
507 
508  try
509  {
510  if( m_textEditor->SaveFile( rulesFilepath ) )
511  {
512  m_frame->GetBoard()->GetDesignSettings().m_DRCEngine->InitEngine( rulesFilepath );
513  return true;
514  }
515  }
516  catch( PARSE_ERROR& )
517  {
518  // Don't lock them in to the Setup dialog if they have bad rules. They've already
519  // saved them so we can allow an exit.
520  return true;
521  }
522 
523  return false;
524 }
525 
526 
527 void PANEL_SETUP_RULES::OnSyntaxHelp( wxHyperlinkEvent& aEvent )
528 {
529  if( m_helpWindow )
530  {
532  return;
533  }
534 
535  wxString msg =
536 #include "dialogs/panel_setup_rules_help_md.h"
537  ;
538 
539 #ifdef __WXMAC__
540  msg.Replace( "Ctrl+", "Cmd+" );
541 #endif
542 
543  m_helpWindow = new HTML_MESSAGE_BOX( nullptr, _( "Syntax Help" ) );
544  m_helpWindow->SetDialogSizeInDU( 320, 320 );
545 
546  wxString html_txt;
547  ConvertMarkdown2Html( wxGetTranslation( msg ), html_txt );
548  m_helpWindow->AddHTML_Text( html_txt );
549 
551 }
const PROPERTY_LIST & GetProperties(TYPE_ID aType) const
Return all properties for a specific type.
static PROPERTY_MANAGER & Instance()
Definition: property_mgr.h:65
const wxArrayString GetSignatures() const
bool TransferDataFromWindow() override
wxString GetDesignRulesPath()
Return the absolute path to the design rules file for the currently-loaded board.
This file is part of the common library.
void Clear()
Delete the stored messages.
void OnCompile(wxCommandEvent &event) override
Add cut/copy/paste, dark theme, autocomplete and brace highlighting to a wxStyleTextCtrl instance.
REPORTER & Report(const wxString &aText, SEVERITY aSeverity=RPT_SEVERITY_UNDEFINED) override
Report a string with a given severity.
bool TransferDataToWindow() override
void DoAutocomplete(const wxString &aPartial, const wxArrayString &aTokens)
void Flush()
Build the HTML messages page.
wxBitmapButton * m_compileButton
SCINTILLA_TRICKS * m_scintillaTricks
BOARD_DESIGN_SETTINGS & GetDesignSettings() const
Definition: board.cpp:590
PCB_EDIT_FRAME * m_frame
const wxString ParseProblem()
Definition: ki_exception.h:150
static LIB_SYMBOL * dummy()
Used to draw a dummy shape when a LIB_SYMBOL is not found in library.
Definition: sch_symbol.cpp:72
Class PANEL_SETUP_RULES_BASE.
WX_HTML_REPORT_BOX * m_errorsReport
PAGED_DIALOG * m_Parent
int lineNumber
at which line number, 1 based index.
Definition: ki_exception.h:120
PROJECT & Prj() const
Return a reference to the PROJECT associated with this KIWAY.
void ConvertMarkdown2Html(const wxString &aMarkdownInput, wxString &aHtmlOutput)
void Parse(std::vector< DRC_RULE * > &aRules, REPORTER *aReporter)
~PANEL_SETUP_RULES() override
std::vector< wxString > GetNetClassAssignmentCandidates() const
Return a list of name candidates for netclass assignment.
Definition: board.cpp:1370
#define _(s)
PANEL_SETUP_RULES(PAGED_DIALOG *aParent, PCB_EDIT_FRAME *aFrame)
NETCLASSES & GetNetClasses() const
void onCharHook(wxKeyEvent &aEvent)
bool ConvertSmartQuotesAndDashes(wxString *aString)
Convert curly quotes and em/en dashes to straight quotes and dashes.
void onScintillaCharAdded(wxStyledTextEvent &aEvent)
wxBitmap KiBitmap(BITMAPS aBitmap, int aHeightTag)
Construct a wxBitmap from an image identifier Returns the image from the active theme if the image ha...
Definition: bitmap.cpp:105
void OnSyntaxHelp(wxHyperlinkEvent &aEvent) override
virtual bool IsNullProject() const
Check if this project is a null project (i.e.
Definition: project.cpp:134
void Format(OUTPUTFORMATTER *out, int aNestLevel, int aCtl, const CPTREE &aTree)
Output a PTREE into s-expression format via an OUTPUTFORMATTER derivative.
Definition: ptree.cpp:200
void ShowModeless()
Show a modeless version of the dialog (without an OK button).
void SetDialogSizeInDU(int aWidth, int aHeight)
Set the dialog size, using a "logical" value.
void wxStringSplit(const wxString &aText, wxArrayString &aStrings, wxChar aSplitter)
Split aString to a string list separated at aSplitter.
A filename or source description, a problem input line, a line number, a byte offset,...
Definition: ki_exception.h:118
std::vector< PROPERTY_BASE * > PROPERTY_LIST
Definition: property_mgr.h:44
Information pertinent to a Pcbnew printed circuit board.
Definition: board.h:191
HTML_MESSAGE_BOX * m_helpWindow
void AddHTML_Text(const wxString &message)
Add HTML text (without any change) to message list.
The main frame for Pcbnew.
wxStyledTextCtrl * m_textEditor
int byteIndex
at which byte offset within the line, 1 based index
Definition: ki_exception.h:121
CLASSES_INFO GetAllClasses()
Provide class metadata.Helper macro to map type hashes to names.
Definition: property_mgr.h:62
BOARD * GetBoard() const
bool IsOK(wxWindow *aParent, const wxString &aMessage)
Display a yes/no dialog with aMessage and returns the user response.
Definition: confirm.cpp:323
std::shared_ptr< DRC_ENGINE > m_DRCEngine
Container for design settings for a BOARD object.
void SetModified()
Definition: paged_dialog.h:41
static PCB_EXPR_BUILTIN_FUNCTIONS & Instance()
void OnErrorLinkClicked(wxHtmlLinkEvent &event) override