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