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 #include <pgm_base.h>
42 
44  PANEL_SETUP_RULES_BASE( aParent->GetTreebook() ),
45  m_Parent( aParent ),
46  m_frame( aFrame ),
47  m_scintillaTricks( nullptr ),
48  m_helpWindow( nullptr )
49 {
50  m_scintillaTricks = new SCINTILLA_TRICKS( m_textEditor, wxT( "()" ), false,
51  [this]()
52  {
53  wxPostEvent( m_Parent, wxCommandEvent( wxEVT_COMMAND_BUTTON_CLICKED, wxID_OK ) );
54  } );
55 
56  m_textEditor->AutoCompSetSeparator( '|' );
57 
58  m_netClassRegex.Compile( "^NetClass\\s*[!=]=\\s*$", wxRE_ADVANCED );
59  m_netNameRegex.Compile( "^NetName\\s*[!=]=\\s*$", wxRE_ADVANCED );
60  m_typeRegex.Compile( "^Type\\s*[!=]=\\s*$", wxRE_ADVANCED );
61  m_padTypeRegex.Compile( "^Pad_Type\\s*[!=]=\\s*$", wxRE_ADVANCED );
62  m_pinTypeRegex.Compile( "^Pin_Type\\s*[!=]=\\s*$", wxRE_ADVANCED );
63  m_fabPropRegex.Compile( "^Fabrication_Property\\s*[!=]=\\s*$", wxRE_ADVANCED );
64 
65  m_compileButton->SetBitmap( KiBitmap( BITMAPS::drc ) );
66 
67  m_textEditor->SetZoom( Pgm().GetCommonSettings()->m_Appearance.text_editor_zoom );
68 
69  m_textEditor->UsePopUp( 0 );
70  m_textEditor->Bind( wxEVT_STC_CHARADDED, &PANEL_SETUP_RULES::onScintillaCharAdded, this );
71  m_textEditor->Bind( wxEVT_STC_AUTOCOMP_CHAR_DELETED, &PANEL_SETUP_RULES::onScintillaCharAdded, this );
72  m_textEditor->Bind( wxEVT_CHAR_HOOK, &PANEL_SETUP_RULES::onCharHook, this );
73 }
74 
75 
77 {
78  Pgm().GetCommonSettings()->m_Appearance.text_editor_zoom = m_textEditor->GetZoom();
79 
80  delete m_scintillaTricks;
81 
82  if( m_helpWindow )
83  m_helpWindow->Destroy();
84 };
85 
86 
87 void PANEL_SETUP_RULES::onCharHook( wxKeyEvent& aEvent )
88 {
89  if( aEvent.GetKeyCode() == WXK_ESCAPE && !m_textEditor->AutoCompActive() )
90  {
91  if( m_originalText != m_textEditor->GetText() )
92  {
93  if( !IsOK( this, _( "Cancel Changes?" ) ) )
94  return;
95  }
96  }
97 
98  aEvent.Skip();
99 }
100 
101 
102 void PANEL_SETUP_RULES::OnContextMenu(wxMouseEvent &event)
103 {
104  wxMenu menu;
105  wxString msg;
106 
107  menu.Append( wxID_UNDO, _( "Undo" ) );
108  menu.Append( wxID_REDO, _( "Redo" ) );
109 
110  menu.AppendSeparator();
111 
112  menu.Append( 1, _( "Cut" ) ); // Don't use wxID_CUT, wxID_COPY, etc. On Mac (at least),
113  menu.Append( 2, _( "Copy" ) ); // wxWidgets never delivers them to us.
114  menu.Append( 3, _( "Paste" ) );
115  menu.Append( 4, _( "Delete" ) );
116 
117  menu.AppendSeparator();
118 
119  menu.Append( 5, _( "Select All" ) );
120 
121  menu.AppendSeparator();
122 
123  menu.Append( wxID_ZOOM_IN, _( "Zoom In" ) );
124  menu.Append( wxID_ZOOM_OUT, _( "Zoom Out" ) );
125 
126 
127  switch( GetPopupMenuSelectionFromUser( menu ) )
128  {
129  case wxID_UNDO:
130  m_textEditor->Undo();
131  break;
132  case wxID_REDO:
133  m_textEditor->Redo();
134  break;
135 
136  case 1:
137  m_textEditor->Cut();
138  break;
139  case 2:
140  m_textEditor->Copy();
141  break;
142  case 3:
143  m_textEditor->Paste();
144  break;
145  case 4:
146  {
147  long from, to;
148  m_textEditor->GetSelection( &from, &to );
149 
150  if( to > from )
151  m_textEditor->DeleteRange( from, to );
152 
153  break;
154  }
155 
156  case 5:
157  m_textEditor->SelectAll();
158  break;
159 
160  case wxID_ZOOM_IN:
161  m_textEditor->ZoomIn();
162  break;
163  case wxID_ZOOM_OUT:
164  m_textEditor->ZoomOut();
165  break;
166  }
167 }
168 
169 
170 void PANEL_SETUP_RULES::onScintillaCharAdded( wxStyledTextEvent &aEvent )
171 {
173  m_textEditor->SearchAnchor();
174 
175  wxString rules = m_textEditor->GetText();
176  int currentPos = m_textEditor->GetCurrentPos();
177  int startPos = 0;
178 
179  for( int line = m_textEditor->LineFromPosition( currentPos ); line > 0; line-- )
180  {
181  int lineStart = m_textEditor->PositionFromLine( line );
182  wxString beginning = m_textEditor->GetTextRange( lineStart, lineStart + 10 );
183 
184  if( beginning.StartsWith( wxT( "(rule " ) ) )
185  {
186  startPos = lineStart;
187  break;
188  }
189  }
190 
191  enum
192  {
193  NONE,
194  STRING,
195  SEXPR_OPEN,
196  SEXPR_TOKEN,
197  SEXPR_STRING,
198  STRUCT_REF
199  };
200 
201  auto isDisallowToken =
202  []( const wxString& token ) -> bool
203  {
204  return token == wxT( "buried_via" )
205  || token == wxT( "graphic" )
206  || token == wxT( "hole" )
207  || token == wxT( "micro_via" )
208  || token == wxT( "pad" )
209  || token == wxT( "text" )
210  || token == wxT( "track" )
211  || token == wxT( "via" )
212  || token == wxT( "zone" );
213  };
214 
215  std::stack<wxString> sexprs;
216  wxString partial;
217  wxString last;
218  int context = NONE;
219  int expr_context = NONE;
220 
221  for( int i = startPos; i < currentPos; ++i )
222  {
223  wxChar c = m_textEditor->GetCharAt( i );
224 
225  if( c == '\\' )
226  {
227  i++; // skip escaped char
228  }
229  else if( context == STRING )
230  {
231  if( c == '"' )
232  {
233  context = NONE;
234  }
235  else
236  {
237  if( expr_context == STRING )
238  {
239  if( c == '\'' )
240  expr_context = NONE;
241  else
242  partial += c;
243  }
244  else if( c == '\'' )
245  {
246  last = partial;
247  partial = wxEmptyString;
248  expr_context = STRING;
249  }
250  else if( c == '.' )
251  {
252  partial = wxEmptyString;
253  expr_context = STRUCT_REF;
254  }
255  else
256  {
257  partial += c;
258  }
259  }
260  }
261  else if( c == '"' )
262  {
263  last = partial;
264  partial = wxEmptyString;
265  context = STRING;
266  }
267  else if( c == '(' )
268  {
269  if( context == SEXPR_OPEN && !partial.IsEmpty() )
270  {
271  m_textEditor->AutoCompCancel();
272  sexprs.push( partial );
273  }
274 
275  partial = wxEmptyString;
276  context = SEXPR_OPEN;
277  }
278  else if( c == ')' )
279  {
280  while( !sexprs.empty() && ( sexprs.top() == wxT( "assertion" )
281  || sexprs.top() == wxT( "disallow" )
282  || isDisallowToken( sexprs.top() )
283  || sexprs.top() == wxT( "min_resolved_spokes" )
284  || sexprs.top() == wxT( "zone_connection" ) ) )
285  {
286  sexprs.pop();
287  }
288 
289  if( !sexprs.empty() )
290  sexprs.pop();
291 
292  context = NONE;
293  }
294  else if( c == ' ' )
295  {
296  if( context == SEXPR_OPEN && ( partial == wxT( "constraint" )
297  || partial == wxT( "disallow" )
298  || partial == wxT( "layer" )
299  || partial == wxT( "severity" ) ) )
300  {
301  m_textEditor->AutoCompCancel();
302  sexprs.push( partial );
303 
304  partial = wxEmptyString;
305  context = SEXPR_TOKEN;
306  continue;
307  }
308  else if( partial == wxT( "disallow" )
309  || isDisallowToken( partial )
310  || partial == wxT( "min_resolved_spokes" )
311  || partial == wxT( "zone_connection" ) )
312  {
313  m_textEditor->AutoCompCancel();
314  sexprs.push( partial );
315 
316  partial = wxEmptyString;
317  context = SEXPR_TOKEN;
318  continue;
319  }
320  else if( partial == wxT( "rule" )
321  || partial == wxT( "assertion" )
322  || partial == wxT( "condition" ) )
323  {
324  m_textEditor->AutoCompCancel();
325  sexprs.push( partial );
326 
327  partial = wxEmptyString;
328  context = SEXPR_STRING;
329  continue;
330  }
331 
332  context = NONE;
333  }
334  else
335  {
336  partial += c;
337  }
338  }
339 
340  wxString tokens;
341 
342  if( context == SEXPR_OPEN )
343  {
344  if( sexprs.empty() )
345  {
346  tokens = wxT( "rule|"
347  "version" );
348  }
349  else if( sexprs.top() == wxT( "rule" ) )
350  {
351  tokens = wxT( "condition|"
352  "constraint|"
353  "layer" );
354  }
355  else if( sexprs.top() == wxT( "constraint" ) )
356  {
357  tokens = wxT( "max|min|opt" );
358  }
359  }
360  else if( context == SEXPR_TOKEN )
361  {
362  if( sexprs.empty() )
363  {
364  /* badly formed grammar */
365  }
366  else if( sexprs.top() == wxT( "constraint" ) )
367  {
368  tokens = wxT( "annular_width|"
369  "clearance|"
370  "courtyard_clearance|"
371  "diff_pair_gap|"
372  "diff_pair_uncoupled|"
373  "disallow|"
374  "edge_clearance|"
375  "length|"
376  "hole_clearance|"
377  "hole_size|"
378  "hole_to_hole|"
379  "silk_clearance|"
380  "skew|"
381  "track_width|"
382  "via_count|"
383  "via_diameter" );
384  }
385  else if( sexprs.top() == wxT( "disallow" ) || isDisallowToken( sexprs.top() ) )
386  {
387  tokens = wxT( "buried_via|"
388  "graphic|"
389  "hole|"
390  "micro_via|"
391  "pad|"
392  "text|"
393  "track|"
394  "via|"
395  "zone" );
396  }
397  else if( sexprs.top() == wxT( "layer" ) )
398  {
399  tokens = wxT( "inner|outer|\"x\"" );
400  }
401  else if( sexprs.top() == wxT( "severity" ) )
402  {
403  tokens = wxT( "warning|error|ignore|exclusion" );
404  }
405  }
406  else if( context == STRING && !sexprs.empty() && sexprs.top() == wxT( "condition" ) )
407  {
408  if( expr_context == STRUCT_REF )
409  {
411  std::set<wxString> propNames;
412 
413  for( const PROPERTY_MANAGER::CLASS_INFO& cls : propMgr.GetAllClasses() )
414  {
415  const PROPERTY_LIST& props = propMgr.GetProperties( cls.type );
416 
417  for( PROPERTY_BASE* prop : props )
418  {
419  wxString ref( prop->Name() );
420  ref.Replace( wxT( " " ), wxT( "_" ) );
421  propNames.insert( ref );
422  }
423  }
424 
425  for( const wxString& propName : propNames )
426  tokens += wxT( "|" ) + propName;
427 
429 
430  for( const wxString& funcSig : functions.GetSignatures() )
431  tokens += wxT( "|" ) + funcSig;
432  }
433  else if( expr_context == STRING )
434  {
435  if( m_netClassRegex.Matches( last ) )
436  {
437  BOARD* board = m_frame->GetBoard();
439 
440  for( const std::pair<const wxString, NETCLASSPTR>& entry : bds.GetNetClasses() )
441  tokens += wxT( "|" ) + entry.first;
442  }
443  else if( m_netNameRegex.Matches( last ) )
444  {
445  BOARD* board = m_frame->GetBoard();
446 
447  for( const wxString& netnameCandidate : board->GetNetClassAssignmentCandidates() )
448  tokens += wxT( "|" ) + netnameCandidate;
449  }
450  else if( m_typeRegex.Matches( last ) )
451  {
452  tokens = wxT( "Dimension|"
453  "Footprint|"
454  "Graphic|"
455  "Group|"
456  "Leader|"
457  "Pad|"
458  "Target|"
459  "Text|"
460  "Track|"
461  "Via|"
462  "Zone" );
463  }
464  else if( m_padTypeRegex.Matches( last ) )
465  {
466  tokens = wxT( "Through-hole|"
467  "SMD|"
468  "Edge connector|"
469  "NPTH, mechanical" );
470  }
471  else if( m_pinTypeRegex.Matches( last ) )
472  {
473  tokens = wxT( "Input|"
474  "Output|"
475  "Bidirectional|"
476  "Tri-state|"
477  "Passive|"
478  "Free|"
479  "Unspecified|"
480  "Power input|"
481  "Power output|"
482  "Open collector|"
483  "Open emitter|"
484  "Unconnected" );
485  }
486  else if( m_fabPropRegex.Matches( last ) )
487  {
488  tokens = wxT( "None|"
489  "BGA pad|"
490  "Fiducial, global to board|"
491  "Fiducial, local to footprint|"
492  "Test point pad|"
493  "Heatsink pad|"
494  "Castellated pad" );
495  }
496  }
497  }
498 
499  if( !tokens.IsEmpty() )
500  m_scintillaTricks->DoAutocomplete( partial, wxSplit( tokens, '|' ) );
501 }
502 
503 
504 void PANEL_SETUP_RULES::OnCompile( wxCommandEvent& event )
505 {
507 
508  try
509  {
510  std::vector<DRC_RULE*> dummyRules;
511 
512  DRC_RULES_PARSER parser( m_textEditor->GetText(), _( "DRC rules" ) );
513 
514  parser.Parse( dummyRules, m_errorsReport );
515  }
516  catch( PARSE_ERROR& pe )
517  {
518  wxString msg = wxString::Format( wxT( "%s <a href='%d:%d'>%s</a>%s" ),
519  _( "ERROR:" ),
520  pe.lineNumber,
521  pe.byteIndex,
522  pe.ParseProblem(),
523  wxEmptyString );
524 
526  }
527 
529 }
530 
531 
532 void PANEL_SETUP_RULES::OnErrorLinkClicked( wxHtmlLinkEvent& event )
533 {
534  wxString link = event.GetLinkInfo().GetHref();
535  wxArrayString parts;
536  long line = 0, offset = 0;
537 
538  wxStringSplit( link, parts, ':' );
539 
540  if( parts.size() > 1 )
541  {
542  parts[0].ToLong( &line );
543  parts[1].ToLong( &offset );
544  }
545 
546  int pos = m_textEditor->PositionFromLine( line - 1 ) + ( offset - 1 );
547 
548  m_textEditor->GotoPos( pos );
549 
550  m_textEditor->SetFocus();
551 }
552 
553 
555 {
556  wxFileName rulesFile( m_frame->GetDesignRulesPath() );
557 
558  if( rulesFile.FileExists() )
559  {
560  wxTextFile file( rulesFile.GetFullPath() );
561 
562  if( file.Open() )
563  {
564  for ( wxString str = file.GetFirstLine(); !file.Eof(); str = file.GetNextLine() )
565  {
567  m_textEditor->AddText( str << '\n' );
568  }
569 
570  wxCommandEvent dummy;
571  OnCompile( dummy );
572  }
573  }
574 
575  m_originalText = m_textEditor->GetText();
576 
577  if( m_frame->Prj().IsNullProject() )
578  {
579  m_textEditor->ClearAll();
580  m_textEditor->AddText( _( "Design rules cannot be added without a project" ) );
581  m_textEditor->Disable();
582  }
583 
584  return true;
585 }
586 
587 
589 {
590  if( m_originalText == m_textEditor->GetText() )
591  return true;
592 
593  if( m_frame->Prj().IsNullProject() )
594  return true;
595 
596  wxString rulesFilepath = m_frame->GetDesignRulesPath();
597 
598  try
599  {
600  if( m_textEditor->SaveFile( rulesFilepath ) )
601  {
602  m_frame->GetBoard()->GetDesignSettings().m_DRCEngine->InitEngine( rulesFilepath );
603  return true;
604  }
605  }
606  catch( PARSE_ERROR& )
607  {
608  // Don't lock them in to the Setup dialog if they have bad rules. They've already
609  // saved them so we can allow an exit.
610  return true;
611  }
612 
613  return false;
614 }
615 
616 
617 void PANEL_SETUP_RULES::OnSyntaxHelp( wxHyperlinkEvent& aEvent )
618 {
619  if( m_helpWindow )
620  {
622  return;
623  }
624 
625  wxString msg =
626 #include "dialogs/panel_setup_rules_help_md.h"
627  ;
628 
629 #ifdef __WXMAC__
630  msg.Replace( wxT( "Ctrl+" ), wxT( "Cmd+" ) );
631 #endif
632 
633  m_helpWindow = new HTML_MESSAGE_BOX( nullptr, _( "Syntax Help" ) );
634  m_helpWindow->SetDialogSizeInDU( 320, 320 );
635 
636  wxString html_txt;
637  ConvertMarkdown2Html( wxGetTranslation( msg ), html_txt );
638  m_helpWindow->AddHTML_Text( html_txt );
639 
641 }
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
KIWAY Kiway & Pgm(), KFCTL_STANDALONE
The global Program "get" accessor.
Definition: single_top.cpp:106
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 OnContextMenu(wxMouseEvent &event) override
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:1373
#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
see class PGM_BASE
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