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