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.

424 lines
13 KiB

4 years ago
4 years ago
  1. /*
  2. * This program source code file is part of KiCad, a free EDA CAD application.
  3. *
  4. * Copyright (C) 2020-2022 KiCad Developers, see change_log.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 <wx/stc/stc.h>
  26. #include <gal/color4d.h>
  27. #include <dialog_shim.h>
  28. #include <wx/clipbrd.h>
  29. #include <wx/log.h>
  30. #include <wx/settings.h>
  31. #include <confirm.h>
  32. SCINTILLA_TRICKS::SCINTILLA_TRICKS( wxStyledTextCtrl* aScintilla, const wxString& aBraces,
  33. bool aSingleLine, std::function<void()> aReturnCallback ) :
  34. m_te( aScintilla ),
  35. m_braces( aBraces ),
  36. m_lastCaretPos( -1 ),
  37. m_lastSelStart( -1 ),
  38. m_lastSelEnd( -1 ),
  39. m_suppressAutocomplete( false ),
  40. m_singleLine( aSingleLine ),
  41. m_returnCallback( aReturnCallback )
  42. {
  43. // Always use LF as eol char, regardless the platform
  44. m_te->SetEOLMode( wxSTC_EOL_LF );
  45. // A hack which causes Scintilla to auto-size the text editor canvas
  46. // See: https://github.com/jacobslusser/ScintillaNET/issues/216
  47. m_te->SetScrollWidth( 1 );
  48. m_te->SetScrollWidthTracking( true );
  49. setupStyles();
  50. // Set up autocomplete
  51. m_te->AutoCompSetIgnoreCase( true );
  52. m_te->AutoCompSetFillUps( m_braces[1] );
  53. m_te->AutoCompSetMaxHeight( 20 );
  54. // Hook up events
  55. m_te->Bind( wxEVT_STC_UPDATEUI, &SCINTILLA_TRICKS::onScintillaUpdateUI, this );
  56. // Dispatch command-keys in Scintilla control.
  57. m_te->Bind( wxEVT_CHAR_HOOK, &SCINTILLA_TRICKS::onCharHook, this );
  58. m_te->Bind( wxEVT_SYS_COLOUR_CHANGED,
  59. wxSysColourChangedEventHandler( SCINTILLA_TRICKS::onThemeChanged ), this );
  60. }
  61. void SCINTILLA_TRICKS::onThemeChanged( wxSysColourChangedEvent &aEvent )
  62. {
  63. setupStyles();
  64. aEvent.Skip();
  65. }
  66. void SCINTILLA_TRICKS::setupStyles()
  67. {
  68. wxTextCtrl dummy( m_te->GetParent(), wxID_ANY );
  69. KIGFX::COLOR4D foreground = dummy.GetForegroundColour();
  70. KIGFX::COLOR4D background = dummy.GetBackgroundColour();
  71. KIGFX::COLOR4D highlight = wxSystemSettings::GetColour( wxSYS_COLOUR_HIGHLIGHT );
  72. KIGFX::COLOR4D highlightText = wxSystemSettings::GetColour( wxSYS_COLOUR_HIGHLIGHTTEXT );
  73. m_te->StyleSetForeground( wxSTC_STYLE_DEFAULT, foreground.ToColour() );
  74. m_te->StyleSetBackground( wxSTC_STYLE_DEFAULT, background.ToColour() );
  75. m_te->StyleClearAll();
  76. // Scintilla doesn't handle alpha channel, which at least OSX uses in some highlight colours,
  77. // such as "graphite".
  78. highlight = highlight.Mix( background, highlight.a ).WithAlpha( 1.0 );
  79. highlightText = highlightText.Mix( background, highlightText.a ).WithAlpha( 1.0 );
  80. m_te->SetSelForeground( true, highlightText.ToColour() );
  81. m_te->SetSelBackground( true, highlight.ToColour() );
  82. m_te->SetCaretForeground( foreground.ToColour() );
  83. if( !m_singleLine )
  84. {
  85. // Set a monospace font with a tab width of 4. This is the closest we can get to having
  86. // Scintilla mimic the stroke font's tab positioning.
  87. wxFont fixedFont = KIUI::GetMonospacedUIFont();
  88. for( size_t i = 0; i < wxSTC_STYLE_MAX; ++i )
  89. m_te->StyleSetFont( i, fixedFont );
  90. m_te->SetTabWidth( 4 );
  91. }
  92. // Set up the brace highlighting. Scintilla doesn't handle alpha, so we construct our own
  93. // 20% wash by blending with the background.
  94. KIGFX::COLOR4D braceText = foreground;
  95. KIGFX::COLOR4D braceHighlight = braceText.Mix( background, 0.2 );
  96. m_te->StyleSetForeground( wxSTC_STYLE_BRACELIGHT, highlightText.ToColour() );
  97. m_te->StyleSetBackground( wxSTC_STYLE_BRACELIGHT, braceHighlight.ToColour() );
  98. m_te->StyleSetForeground( wxSTC_STYLE_BRACEBAD, *wxRED );
  99. }
  100. bool isCtrlSlash( wxKeyEvent& aEvent )
  101. {
  102. if( !aEvent.ControlDown() || aEvent.MetaDown() )
  103. return false;
  104. if( aEvent.GetUnicodeKey() == '/' )
  105. return true;
  106. // OK, now the wxWidgets hacks start.
  107. // (We should abandon these if https://trac.wxwidgets.org/ticket/18911 gets resolved.)
  108. // Many Latin America and European keyboars have have the / over the 7. We know that
  109. // wxWidgets messes this up and returns Shift+7 through GetUnicodeKey(). However, other
  110. // keyboards (such as France and Belgium) have 7 in the shifted position, so a Shift+7
  111. // *could* be legitimate.
  112. // However, we *are* checking Ctrl, so to assume any Shift+7 is a Ctrl-/ really only
  113. // disallows Ctrl+Shift+7 from doing something else, which is probably OK. (This routine
  114. // is only used in the Scintilla editor, not in the rest of Kicad.)
  115. // The other main shifted loation of / is over : (France and Belgium), so we'll sacrifice
  116. // Ctrl+Shift+: too.
  117. if( aEvent.ShiftDown() && ( aEvent.GetUnicodeKey() == '7' || aEvent.GetUnicodeKey() == ':' ) )
  118. return true;
  119. // A few keyboards have / in an Alt position. Since we're expressly not checking Alt for
  120. // up or down, those should work. However, if they don't, there's room below for yet
  121. // another hack....
  122. return false;
  123. }
  124. void SCINTILLA_TRICKS::onCharHook( wxKeyEvent& aEvent )
  125. {
  126. wxString c = aEvent.GetUnicodeKey();
  127. if( !isalpha( aEvent.GetKeyCode() ) )
  128. m_suppressAutocomplete = false;
  129. if( aEvent.GetKeyCode() == WXK_RETURN && ( m_singleLine || aEvent.ShiftDown() ) )
  130. {
  131. m_returnCallback();
  132. }
  133. else if( ConvertSmartQuotesAndDashes( &c ) )
  134. {
  135. m_te->AddText( c );
  136. }
  137. else if( aEvent.GetKeyCode() == WXK_TAB )
  138. {
  139. if( aEvent.ControlDown() )
  140. {
  141. int flags = 0;
  142. if( !aEvent.ShiftDown() )
  143. flags |= wxNavigationKeyEvent::IsForward;
  144. wxWindow* parent = m_te->GetParent();
  145. while( parent && dynamic_cast<DIALOG_SHIM*>( parent ) == nullptr )
  146. parent = parent->GetParent();
  147. if( parent )
  148. parent->NavigateIn( flags );
  149. }
  150. else
  151. {
  152. m_te->Tab();
  153. }
  154. }
  155. else if( aEvent.GetModifiers() == wxMOD_CONTROL && aEvent.GetKeyCode() == 'Z' )
  156. {
  157. m_te->Undo();
  158. }
  159. else if( ( aEvent.GetModifiers() == wxMOD_SHIFT+wxMOD_CONTROL && aEvent.GetKeyCode() == 'Z' )
  160. || ( aEvent.GetModifiers() == wxMOD_CONTROL && aEvent.GetKeyCode() == 'Y' ) )
  161. {
  162. m_te->Redo();
  163. }
  164. else if( aEvent.GetModifiers() == wxMOD_CONTROL && aEvent.GetKeyCode() == 'A' )
  165. {
  166. m_te->SelectAll();
  167. }
  168. else if( aEvent.GetModifiers() == wxMOD_CONTROL && aEvent.GetKeyCode() == 'X' )
  169. {
  170. m_te->Cut();
  171. }
  172. else if( aEvent.GetModifiers() == wxMOD_CONTROL && aEvent.GetKeyCode() == 'C' )
  173. {
  174. m_te->Copy();
  175. }
  176. else if( aEvent.GetModifiers() == wxMOD_CONTROL && aEvent.GetKeyCode() == 'V' )
  177. {
  178. if( m_te->GetSelectionEnd() > m_te->GetSelectionStart() )
  179. m_te->DeleteBack();
  180. wxLogNull doNotLog; // disable logging of failed clipboard actions
  181. if( wxTheClipboard->Open() )
  182. {
  183. if( wxTheClipboard->IsSupported( wxDF_TEXT ) ||
  184. wxTheClipboard->IsSupported( wxDF_UNICODETEXT ) )
  185. {
  186. wxTextDataObject data;
  187. wxString str;
  188. wxTheClipboard->GetData( data );
  189. str = data.GetText();
  190. ConvertSmartQuotesAndDashes( &str );
  191. m_te->BeginUndoAction();
  192. m_te->AddText( str );
  193. m_te->EndUndoAction();
  194. }
  195. wxTheClipboard->Close();
  196. }
  197. }
  198. else if( aEvent.GetKeyCode() == WXK_BACK )
  199. {
  200. m_te->DeleteBack();
  201. }
  202. else if( aEvent.GetKeyCode() == WXK_DELETE )
  203. {
  204. if( m_te->GetSelectionEnd() == m_te->GetSelectionStart() )
  205. m_te->CharRightExtend();
  206. if( m_te->GetSelectionEnd() > m_te->GetSelectionStart() )
  207. m_te->DeleteBack();
  208. }
  209. else if( aEvent.GetKeyCode() == WXK_ESCAPE )
  210. {
  211. if( m_te->AutoCompActive() )
  212. {
  213. m_te->AutoCompCancel();
  214. m_suppressAutocomplete = true; // Don't run autocomplete again on the next char...
  215. }
  216. else
  217. {
  218. aEvent.Skip();
  219. }
  220. }
  221. else if( isCtrlSlash( aEvent ) )
  222. {
  223. int startLine = m_te->LineFromPosition( m_te->GetSelectionStart() );
  224. int endLine = m_te->LineFromPosition( m_te->GetSelectionEnd() );
  225. bool comment = firstNonWhitespace( startLine ) != '#';
  226. int whitespaceCount;
  227. m_te->BeginUndoAction();
  228. for( int ii = startLine; ii <= endLine; ++ii )
  229. {
  230. if( comment )
  231. m_te->InsertText( m_te->PositionFromLine( ii ), wxT( "#" ) );
  232. else if( firstNonWhitespace( ii, &whitespaceCount ) == '#' )
  233. m_te->DeleteRange( m_te->PositionFromLine( ii ) + whitespaceCount, 1 );
  234. }
  235. m_te->SetSelection( m_te->PositionFromLine( startLine ),
  236. m_te->PositionFromLine( endLine ) + m_te->GetLineLength( endLine ) );
  237. m_te->EndUndoAction();
  238. }
  239. #ifdef __WXMAC__
  240. else if( aEvent.GetModifiers() == wxMOD_RAW_CONTROL && aEvent.GetKeyCode() == 'A' )
  241. {
  242. m_te->HomeWrap();
  243. }
  244. else if( aEvent.GetModifiers() == wxMOD_RAW_CONTROL && aEvent.GetKeyCode() == 'E' )
  245. {
  246. m_te->LineEndWrap();
  247. }
  248. #endif
  249. else if( aEvent.GetKeyCode() == WXK_SPECIAL20 )
  250. {
  251. // Proxy for a wxSysColourChangedEvent
  252. setupStyles();
  253. }
  254. else
  255. {
  256. aEvent.Skip();
  257. }
  258. }
  259. int SCINTILLA_TRICKS::firstNonWhitespace( int aLine, int* aWhitespaceCharCount )
  260. {
  261. int lineStart = m_te->PositionFromLine( aLine );
  262. if( aWhitespaceCharCount )
  263. *aWhitespaceCharCount = 0;
  264. for( int ii = 0; ii < m_te->GetLineLength( aLine ); ++ii )
  265. {
  266. int c = m_te->GetCharAt( lineStart + ii );
  267. if( c == ' ' || c == '\t' )
  268. {
  269. if( aWhitespaceCharCount )
  270. *aWhitespaceCharCount += 1;
  271. continue;
  272. }
  273. else
  274. {
  275. return c;
  276. }
  277. }
  278. return '\r';
  279. }
  280. void SCINTILLA_TRICKS::onScintillaUpdateUI( wxStyledTextEvent& aEvent )
  281. {
  282. auto isBrace = [this]( int c ) -> bool
  283. {
  284. return m_braces.Find( (wxChar) c ) >= 0;
  285. };
  286. // Has the caret changed position?
  287. int caretPos = m_te->GetCurrentPos();
  288. int selStart = m_te->GetSelectionStart();
  289. int selEnd = m_te->GetSelectionEnd();
  290. if( m_lastCaretPos != caretPos || m_lastSelStart != selStart || m_lastSelEnd != selEnd )
  291. {
  292. m_lastCaretPos = caretPos;
  293. m_lastSelStart = selStart;
  294. m_lastSelEnd = selEnd;
  295. int bracePos1 = -1;
  296. int bracePos2 = -1;
  297. // Is there a brace to the left or right?
  298. if( caretPos > 0 && isBrace( m_te->GetCharAt( caretPos-1 ) ) )
  299. bracePos1 = ( caretPos - 1 );
  300. else if( isBrace( m_te->GetCharAt( caretPos ) ) )
  301. bracePos1 = caretPos;
  302. if( bracePos1 >= 0 )
  303. {
  304. // Find the matching brace
  305. bracePos2 = m_te->BraceMatch( bracePos1 );
  306. if( bracePos2 == -1 )
  307. {
  308. m_te->BraceBadLight( bracePos1 );
  309. m_te->SetHighlightGuide( 0 );
  310. }
  311. else
  312. {
  313. m_te->BraceHighlight( bracePos1, bracePos2 );
  314. m_te->SetHighlightGuide( m_te->GetColumn( bracePos1 ) );
  315. }
  316. }
  317. else
  318. {
  319. // Turn off brace matching
  320. m_te->BraceHighlight( -1, -1 );
  321. m_te->SetHighlightGuide( 0 );
  322. }
  323. }
  324. }
  325. void SCINTILLA_TRICKS::DoAutocomplete( const wxString& aPartial, const wxArrayString& aTokens )
  326. {
  327. if( m_suppressAutocomplete )
  328. return;
  329. wxArrayString matchedTokens;
  330. wxString filter = wxT( "*" ) + aPartial.Lower() + wxT( "*" );
  331. for( const wxString& token : aTokens )
  332. {
  333. if( token.Lower().Matches( filter ) )
  334. matchedTokens.push_back( token );
  335. }
  336. if( matchedTokens.size() > 0 )
  337. {
  338. // NB: tokens MUST be in alphabetical order because the Scintilla engine is going
  339. // to do a binary search on them
  340. matchedTokens.Sort( []( const wxString& first, const wxString& second ) -> int
  341. {
  342. return first.CmpNoCase( second );
  343. });
  344. m_te->AutoCompShow( aPartial.size(), wxJoin( matchedTokens, m_te->AutoCompGetSeparator() ) );
  345. }
  346. }
  347. void SCINTILLA_TRICKS::CancelAutocomplete()
  348. {
  349. m_te->AutoCompCancel();
  350. }