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