You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

639 lines
20 KiB

4 years ago
  1. /*
  2. * This program source code file is part of KiCad, a free EDA CAD application.
  3. *
  4. * Copyright The 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. #include <string_utils.h>
  24. #include <scintilla_tricks.h>
  25. #include <widgets/wx_grid.h>
  26. #include <widgets/ui_common.h>
  27. #include <wx/stc/stc.h>
  28. #include <gal/color4d.h>
  29. #include <dialog_shim.h>
  30. #include <wx/clipbrd.h>
  31. #include <wx/log.h>
  32. #include <wx/settings.h>
  33. #include <confirm.h>
  34. SCINTILLA_TRICKS::SCINTILLA_TRICKS( wxStyledTextCtrl* aScintilla, const wxString& aBraces,
  35. bool aSingleLine,
  36. std::function<void( wxKeyEvent& )> onAcceptFn,
  37. std::function<void( wxStyledTextEvent& )> onCharAddedFn ) :
  38. m_te( aScintilla ),
  39. m_braces( aBraces ),
  40. m_lastCaretPos( -1 ),
  41. m_lastSelStart( -1 ),
  42. m_lastSelEnd( -1 ),
  43. m_suppressAutocomplete( false ),
  44. m_singleLine( aSingleLine ),
  45. m_onAcceptFn( std::move( onAcceptFn ) ),
  46. m_onCharAddedFn( std::move( onCharAddedFn ) )
  47. {
  48. // Always use LF as eol char, regardless the platform
  49. m_te->SetEOLMode( wxSTC_EOL_LF );
  50. // A hack which causes Scintilla to auto-size the text editor canvas
  51. // See: https://github.com/jacobslusser/ScintillaNET/issues/216
  52. m_te->SetScrollWidth( 1 );
  53. m_te->SetScrollWidthTracking( true );
  54. if( m_singleLine )
  55. {
  56. m_te->SetUseVerticalScrollBar( false );
  57. m_te->SetUseHorizontalScrollBar( false );
  58. }
  59. setupStyles();
  60. // Set up autocomplete
  61. m_te->AutoCompSetIgnoreCase( true );
  62. m_te->AutoCompSetMaxHeight( 20 );
  63. if( aBraces.Length() >= 2 )
  64. m_te->AutoCompSetFillUps( m_braces[1] );
  65. // Hook up events
  66. m_te->Bind( wxEVT_STC_UPDATEUI, &SCINTILLA_TRICKS::onScintillaUpdateUI, this );
  67. m_te->Bind( wxEVT_STC_MODIFIED, &SCINTILLA_TRICKS::onModified, this );
  68. // Handle autocomplete
  69. m_te->Bind( wxEVT_STC_CHARADDED, &SCINTILLA_TRICKS::onChar, this );
  70. m_te->Bind( wxEVT_STC_AUTOCOMP_CHAR_DELETED, &SCINTILLA_TRICKS::onChar, this );
  71. // Dispatch command-keys in Scintilla control.
  72. m_te->Bind( wxEVT_CHAR_HOOK, &SCINTILLA_TRICKS::onCharHook, this );
  73. m_te->Bind( wxEVT_SYS_COLOUR_CHANGED,
  74. wxSysColourChangedEventHandler( SCINTILLA_TRICKS::onThemeChanged ), this );
  75. }
  76. void SCINTILLA_TRICKS::onThemeChanged( wxSysColourChangedEvent &aEvent )
  77. {
  78. setupStyles();
  79. aEvent.Skip();
  80. }
  81. void SCINTILLA_TRICKS::setupStyles()
  82. {
  83. wxTextCtrl dummy( m_te->GetParent(), wxID_ANY );
  84. KIGFX::COLOR4D foreground = dummy.GetForegroundColour();
  85. KIGFX::COLOR4D background = dummy.GetBackgroundColour();
  86. KIGFX::COLOR4D highlight = wxSystemSettings::GetColour( wxSYS_COLOUR_HIGHLIGHT );
  87. KIGFX::COLOR4D highlightText = wxSystemSettings::GetColour( wxSYS_COLOUR_HIGHLIGHTTEXT );
  88. m_te->StyleSetForeground( wxSTC_STYLE_DEFAULT, foreground.ToColour() );
  89. m_te->StyleSetBackground( wxSTC_STYLE_DEFAULT, background.ToColour() );
  90. m_te->StyleClearAll();
  91. // Scintilla doesn't handle alpha channel, which at least OSX uses in some highlight colours,
  92. // such as "graphite".
  93. highlight = highlight.Mix( background, highlight.a ).WithAlpha( 1.0 );
  94. highlightText = highlightText.Mix( background, highlightText.a ).WithAlpha( 1.0 );
  95. m_te->SetSelForeground( true, highlightText.ToColour() );
  96. m_te->SetSelBackground( true, highlight.ToColour() );
  97. m_te->SetCaretForeground( foreground.ToColour() );
  98. if( !m_singleLine )
  99. {
  100. // Set a monospace font with a tab width of 4. This is the closest we can get to having
  101. // Scintilla mimic the stroke font's tab positioning.
  102. wxFont fixedFont = KIUI::GetMonospacedUIFont();
  103. for( size_t i = 0; i < wxSTC_STYLE_MAX; ++i )
  104. m_te->StyleSetFont( i, fixedFont );
  105. m_te->SetTabWidth( 4 );
  106. }
  107. // Set up the brace highlighting. Scintilla doesn't handle alpha, so we construct our own
  108. // 20% wash by blending with the background.
  109. KIGFX::COLOR4D braceText = foreground;
  110. KIGFX::COLOR4D braceHighlight = braceText.Mix( background, 0.2 );
  111. m_te->StyleSetForeground( wxSTC_STYLE_BRACELIGHT, highlightText.ToColour() );
  112. m_te->StyleSetBackground( wxSTC_STYLE_BRACELIGHT, braceHighlight.ToColour() );
  113. m_te->StyleSetForeground( wxSTC_STYLE_BRACEBAD, *wxRED );
  114. }
  115. bool isCtrlSlash( wxKeyEvent& aEvent )
  116. {
  117. if( !aEvent.ControlDown() || aEvent.MetaDown() )
  118. return false;
  119. if( aEvent.GetUnicodeKey() == '/' )
  120. return true;
  121. // OK, now the wxWidgets hacks start.
  122. // (We should abandon these if https://trac.wxwidgets.org/ticket/18911 gets resolved.)
  123. // Many Latin America and European keyboards have have the / over the 7. We know that
  124. // wxWidgets messes this up and returns Shift+7 through GetUnicodeKey(). However, other
  125. // keyboards (such as France and Belgium) have 7 in the shifted position, so a Shift+7
  126. // *could* be legitimate.
  127. // However, we *are* checking Ctrl, so to assume any Shift+7 is a Ctrl-/ really only
  128. // disallows Ctrl+Shift+7 from doing something else, which is probably OK. (This routine
  129. // is only used in the Scintilla editor, not in the rest of KiCad.)
  130. // The other main shifted location of / is over : (France and Belgium), so we'll sacrifice
  131. // Ctrl+Shift+: too.
  132. if( aEvent.ShiftDown() && ( aEvent.GetUnicodeKey() == '7' || aEvent.GetUnicodeKey() == ':' ) )
  133. return true;
  134. // A few keyboards have / in an Alt position. Since we're expressly not checking Alt for
  135. // up or down, those should work. However, if they don't, there's room below for yet
  136. // another hack....
  137. return false;
  138. }
  139. void SCINTILLA_TRICKS::onChar( wxStyledTextEvent& aEvent )
  140. {
  141. m_onCharAddedFn( aEvent );
  142. }
  143. void SCINTILLA_TRICKS::onModified( wxStyledTextEvent& aEvent )
  144. {
  145. if( m_singleLine )
  146. {
  147. wxString curr_text = m_te->GetText();
  148. if( curr_text.Contains( wxS( "\n" ) ) || curr_text.Contains( wxS( "\r" ) ) )
  149. {
  150. // Scintilla won't allow us to call SetText() from within this event processor,
  151. // so we have to delay the processing.
  152. CallAfter( [this]()
  153. {
  154. wxString text = m_te->GetText();
  155. int currpos = m_te->GetCurrentPos();
  156. text.Replace( wxS( "\n" ), wxS( "" ) );
  157. text.Replace( wxS( "\r" ), wxS( "" ) );
  158. m_te->SetText( text );
  159. m_te->GotoPos( currpos-1 );
  160. } );
  161. }
  162. }
  163. }
  164. void SCINTILLA_TRICKS::onCharHook( wxKeyEvent& aEvent )
  165. {
  166. wxString c = aEvent.GetUnicodeKey();
  167. if( m_te->AutoCompActive() )
  168. {
  169. if( aEvent.GetKeyCode() == WXK_ESCAPE )
  170. {
  171. m_te->AutoCompCancel();
  172. m_suppressAutocomplete = true; // Don't run autocomplete again on the next char...
  173. }
  174. else if( aEvent.GetKeyCode() == WXK_RETURN || aEvent.GetKeyCode() == WXK_NUMPAD_ENTER )
  175. {
  176. int start = m_te->AutoCompPosStart();
  177. m_te->AutoCompComplete();
  178. int finish = m_te->GetCurrentPos();
  179. if( finish > start )
  180. {
  181. // Select the last substitution token (if any) in the autocompleted text
  182. int selStart = m_te->FindText( finish, start, "<" );
  183. int selEnd = m_te->FindText( finish, start, ">" );
  184. if( selStart > start && selEnd <= finish && selEnd > selStart )
  185. m_te->SetSelection( selStart, selEnd + 1 );
  186. }
  187. }
  188. else
  189. {
  190. aEvent.Skip();
  191. }
  192. return;
  193. }
  194. #ifdef __WXMAC__
  195. if( aEvent.GetModifiers() == wxMOD_RAW_CONTROL && aEvent.GetKeyCode() == WXK_SPACE )
  196. #else
  197. if( aEvent.GetModifiers() == wxMOD_CONTROL && aEvent.GetKeyCode() == WXK_SPACE )
  198. #endif
  199. {
  200. m_suppressAutocomplete = false;
  201. wxStyledTextEvent event;
  202. event.SetKey( ' ' );
  203. event.SetModifiers( wxMOD_CONTROL );
  204. m_onCharAddedFn( event );
  205. return;
  206. }
  207. if( !isalpha( aEvent.GetKeyCode() ) )
  208. m_suppressAutocomplete = false;
  209. if( ( aEvent.GetKeyCode() == WXK_RETURN || aEvent.GetKeyCode() == WXK_NUMPAD_ENTER )
  210. && ( m_singleLine || aEvent.ShiftDown() ) )
  211. {
  212. m_onAcceptFn( aEvent );
  213. }
  214. else if( ConvertSmartQuotesAndDashes( &c ) )
  215. {
  216. m_te->AddText( c );
  217. }
  218. else if( aEvent.GetKeyCode() == WXK_TAB )
  219. {
  220. wxWindow* ancestor = m_te->GetParent();
  221. while( ancestor && !dynamic_cast<WX_GRID*>( ancestor ) )
  222. ancestor = ancestor->GetParent();
  223. if( aEvent.ControlDown() )
  224. {
  225. int flags = 0;
  226. if( !aEvent.ShiftDown() )
  227. flags |= wxNavigationKeyEvent::IsForward;
  228. if( DIALOG_SHIM* dlg = dynamic_cast<DIALOG_SHIM*>( wxGetTopLevelParent( m_te ) ) )
  229. dlg->NavigateIn( flags );
  230. }
  231. else if( dynamic_cast<WX_GRID*>( ancestor ) )
  232. {
  233. WX_GRID* grid = static_cast<WX_GRID*>( ancestor );
  234. int row = grid->GetGridCursorRow();
  235. int col = grid->GetGridCursorCol();
  236. if( aEvent.ShiftDown() )
  237. {
  238. if( col > 0 )
  239. {
  240. col--;
  241. }
  242. else if( row > 0 )
  243. {
  244. col = (int) grid->GetNumberCols() - 1;
  245. if( row > 0 )
  246. row--;
  247. else
  248. row = (int) grid->GetNumberRows() - 1;
  249. }
  250. }
  251. else
  252. {
  253. if( col < (int) grid->GetNumberCols() - 1 )
  254. {
  255. col++;
  256. }
  257. else if( row < grid->GetNumberRows() - 1 )
  258. {
  259. col = 0;
  260. if( row < grid->GetNumberRows() - 1 )
  261. row++;
  262. else
  263. row = 0;
  264. }
  265. }
  266. grid->SetGridCursor( row, col );
  267. }
  268. else
  269. {
  270. m_te->Tab();
  271. }
  272. }
  273. else if( aEvent.GetModifiers() == wxMOD_CONTROL && aEvent.GetKeyCode() == 'Z' )
  274. {
  275. m_te->Undo();
  276. }
  277. else if( ( aEvent.GetModifiers() == wxMOD_SHIFT+wxMOD_CONTROL && aEvent.GetKeyCode() == 'Z' )
  278. || ( aEvent.GetModifiers() == wxMOD_CONTROL && aEvent.GetKeyCode() == 'Y' ) )
  279. {
  280. m_te->Redo();
  281. }
  282. else if( aEvent.GetModifiers() == wxMOD_CONTROL && aEvent.GetKeyCode() == 'A' )
  283. {
  284. m_te->SelectAll();
  285. }
  286. else if( aEvent.GetModifiers() == wxMOD_CONTROL && aEvent.GetKeyCode() == 'X' )
  287. {
  288. m_te->Cut();
  289. if( wxTheClipboard->Open() )
  290. {
  291. wxTheClipboard->Flush(); // Allow data to be available after closing KiCad
  292. wxTheClipboard->Close();
  293. }
  294. }
  295. else if( aEvent.GetModifiers() == wxMOD_CONTROL && aEvent.GetKeyCode() == 'C' )
  296. {
  297. m_te->Copy();
  298. if( wxTheClipboard->Open() )
  299. {
  300. wxTheClipboard->Flush(); // Allow data to be available after closing KiCad
  301. wxTheClipboard->Close();
  302. }
  303. }
  304. else if( aEvent.GetModifiers() == wxMOD_CONTROL && aEvent.GetKeyCode() == 'V' )
  305. {
  306. if( m_te->GetSelectionEnd() > m_te->GetSelectionStart() )
  307. m_te->DeleteBack();
  308. wxLogNull doNotLog; // disable logging of failed clipboard actions
  309. if( wxTheClipboard->Open() )
  310. {
  311. if( wxTheClipboard->IsSupported( wxDF_TEXT ) ||
  312. wxTheClipboard->IsSupported( wxDF_UNICODETEXT ) )
  313. {
  314. wxTextDataObject data;
  315. wxString str;
  316. wxTheClipboard->GetData( data );
  317. str = data.GetText();
  318. ConvertSmartQuotesAndDashes( &str );
  319. if( m_singleLine )
  320. {
  321. str.Replace( wxS( "\n" ), wxEmptyString );
  322. str.Replace( wxS( "\r" ), wxEmptyString );
  323. }
  324. m_te->BeginUndoAction();
  325. m_te->AddText( str );
  326. m_te->EndUndoAction();
  327. }
  328. wxTheClipboard->Close();
  329. }
  330. }
  331. else if( aEvent.GetKeyCode() == WXK_BACK )
  332. {
  333. if( aEvent.GetModifiers() == wxMOD_CONTROL )
  334. #ifdef __WXMAC__
  335. m_te->HomeExtend();
  336. else if( aEvent.GetModifiers() == wxMOD_ALT )
  337. #endif
  338. m_te->WordLeftExtend();
  339. m_te->DeleteBack();
  340. }
  341. else if( aEvent.GetKeyCode() == WXK_DELETE )
  342. {
  343. if( m_te->GetSelectionEnd() == m_te->GetSelectionStart() )
  344. m_te->CharRightExtend();
  345. if( m_te->GetSelectionEnd() > m_te->GetSelectionStart() )
  346. m_te->DeleteBack();
  347. }
  348. else if( isCtrlSlash( aEvent ) )
  349. {
  350. int startLine = m_te->LineFromPosition( m_te->GetSelectionStart() );
  351. int endLine = m_te->LineFromPosition( m_te->GetSelectionEnd() );
  352. bool comment = firstNonWhitespace( startLine ) != '#';
  353. int whitespaceCount;
  354. m_te->BeginUndoAction();
  355. for( int ii = startLine; ii <= endLine; ++ii )
  356. {
  357. if( comment )
  358. m_te->InsertText( m_te->PositionFromLine( ii ), wxT( "#" ) );
  359. else if( firstNonWhitespace( ii, &whitespaceCount ) == '#' )
  360. m_te->DeleteRange( m_te->PositionFromLine( ii ) + whitespaceCount, 1 );
  361. }
  362. m_te->SetSelection( m_te->PositionFromLine( startLine ),
  363. m_te->PositionFromLine( endLine ) + m_te->GetLineLength( endLine ) );
  364. m_te->EndUndoAction();
  365. }
  366. #ifdef __WXMAC__
  367. else if( aEvent.GetModifiers() == wxMOD_RAW_CONTROL && aEvent.GetKeyCode() == 'A' )
  368. {
  369. m_te->HomeWrap();
  370. }
  371. else if( aEvent.GetModifiers() == wxMOD_RAW_CONTROL && aEvent.GetKeyCode() == 'E' )
  372. {
  373. m_te->LineEndWrap();
  374. }
  375. else if( ( aEvent.GetModifiers() & wxMOD_RAW_CONTROL ) && aEvent.GetKeyCode() == 'B' )
  376. {
  377. if( aEvent.GetModifiers() & wxMOD_ALT )
  378. m_te->WordLeft();
  379. else
  380. m_te->CharLeft();
  381. }
  382. else if( ( aEvent.GetModifiers() & wxMOD_RAW_CONTROL ) && aEvent.GetKeyCode() == 'F' )
  383. {
  384. if( aEvent.GetModifiers() & wxMOD_ALT )
  385. m_te->WordRight();
  386. else
  387. m_te->CharRight();
  388. }
  389. else if( aEvent.GetModifiers() == wxMOD_RAW_CONTROL && aEvent.GetKeyCode() == 'D' )
  390. {
  391. if( m_te->GetSelectionEnd() == m_te->GetSelectionStart() )
  392. m_te->CharRightExtend();
  393. if( m_te->GetSelectionEnd() > m_te->GetSelectionStart() )
  394. m_te->DeleteBack();
  395. }
  396. #endif
  397. else if( aEvent.GetKeyCode() == WXK_SPECIAL20 )
  398. {
  399. // Proxy for a wxSysColourChangedEvent
  400. setupStyles();
  401. }
  402. else
  403. {
  404. aEvent.Skip();
  405. }
  406. }
  407. int SCINTILLA_TRICKS::firstNonWhitespace( int aLine, int* aWhitespaceCharCount )
  408. {
  409. int lineStart = m_te->PositionFromLine( aLine );
  410. if( aWhitespaceCharCount )
  411. *aWhitespaceCharCount = 0;
  412. for( int ii = 0; ii < m_te->GetLineLength( aLine ); ++ii )
  413. {
  414. int c = m_te->GetCharAt( lineStart + ii );
  415. if( c == ' ' || c == '\t' )
  416. {
  417. if( aWhitespaceCharCount )
  418. *aWhitespaceCharCount += 1;
  419. continue;
  420. }
  421. else
  422. {
  423. return c;
  424. }
  425. }
  426. return '\r';
  427. }
  428. void SCINTILLA_TRICKS::onScintillaUpdateUI( wxStyledTextEvent& aEvent )
  429. {
  430. auto isBrace = [this]( int c ) -> bool
  431. {
  432. return m_braces.Find( (wxChar) c ) >= 0;
  433. };
  434. // Has the caret changed position?
  435. int caretPos = m_te->GetCurrentPos();
  436. int selStart = m_te->GetSelectionStart();
  437. int selEnd = m_te->GetSelectionEnd();
  438. if( m_lastCaretPos != caretPos || m_lastSelStart != selStart || m_lastSelEnd != selEnd )
  439. {
  440. m_lastCaretPos = caretPos;
  441. m_lastSelStart = selStart;
  442. m_lastSelEnd = selEnd;
  443. int bracePos1 = -1;
  444. int bracePos2 = -1;
  445. // Is there a brace to the left or right?
  446. if( caretPos > 0 && isBrace( m_te->GetCharAt( caretPos-1 ) ) )
  447. bracePos1 = ( caretPos - 1 );
  448. else if( isBrace( m_te->GetCharAt( caretPos ) ) )
  449. bracePos1 = caretPos;
  450. if( bracePos1 >= 0 )
  451. {
  452. // Find the matching brace
  453. bracePos2 = m_te->BraceMatch( bracePos1 );
  454. if( bracePos2 == -1 )
  455. {
  456. m_te->BraceBadLight( bracePos1 );
  457. m_te->SetHighlightGuide( 0 );
  458. }
  459. else
  460. {
  461. m_te->BraceHighlight( bracePos1, bracePos2 );
  462. m_te->SetHighlightGuide( m_te->GetColumn( bracePos1 ) );
  463. }
  464. }
  465. else
  466. {
  467. // Turn off brace matching
  468. m_te->BraceHighlight( -1, -1 );
  469. m_te->SetHighlightGuide( 0 );
  470. }
  471. }
  472. }
  473. void SCINTILLA_TRICKS::DoTextVarAutocomplete(
  474. const std::function<void( const wxString& xRef, wxArrayString* tokens )>& getTokensFn )
  475. {
  476. wxArrayString autocompleteTokens;
  477. int text_pos = m_te->GetCurrentPos();
  478. int start = m_te->WordStartPosition( text_pos, true );
  479. wxString partial;
  480. auto textVarRef =
  481. [&]( int pos )
  482. {
  483. return pos >= 2 && m_te->GetCharAt( pos-2 ) == '$'
  484. && m_te->GetCharAt( pos-1 ) == '{';
  485. };
  486. // Check for cross-reference
  487. if( start > 1 && m_te->GetCharAt( start-1 ) == ':' )
  488. {
  489. int refStart = m_te->WordStartPosition( start-1, true );
  490. if( textVarRef( refStart ) )
  491. {
  492. partial = m_te->GetRange( start, text_pos );
  493. getTokensFn( m_te->GetRange( refStart, start-1 ), &autocompleteTokens );
  494. }
  495. }
  496. else if( textVarRef( start ) )
  497. {
  498. partial = m_te->GetTextRange( start, text_pos );
  499. getTokensFn( wxEmptyString, &autocompleteTokens );
  500. }
  501. DoAutocomplete( partial, autocompleteTokens );
  502. m_te->SetFocus();
  503. }
  504. void SCINTILLA_TRICKS::DoAutocomplete( const wxString& aPartial, const wxArrayString& aTokens )
  505. {
  506. if( m_suppressAutocomplete )
  507. return;
  508. wxArrayString matchedTokens;
  509. wxString filter = wxT( "*" ) + aPartial.Lower() + wxT( "*" );
  510. for( const wxString& token : aTokens )
  511. {
  512. if( token.Lower().Matches( filter ) )
  513. matchedTokens.push_back( token );
  514. }
  515. if( matchedTokens.size() > 0 )
  516. {
  517. // NB: tokens MUST be in alphabetical order because the Scintilla engine is going
  518. // to do a binary search on them
  519. matchedTokens.Sort( []( const wxString& first, const wxString& second ) -> int
  520. {
  521. return first.CmpNoCase( second );
  522. });
  523. m_te->AutoCompSetSeparator( '\t' );
  524. m_te->AutoCompShow( aPartial.size(), wxJoin( matchedTokens, '\t' ) );
  525. }
  526. }
  527. void SCINTILLA_TRICKS::CancelAutocomplete()
  528. {
  529. m_te->AutoCompCancel();
  530. }