kicad-source/common/scintilla_tricks.cpp
Seth Hillbrand 0b2d4d4879 Revise Copyright statement to align with TLF
Recommendation is to avoid using the year nomenclature as this
information is already encoded in the git repo.  Avoids needing to
repeatly update.

Also updates AUTHORS.txt from current repo with contributor names
2025-01-01 14:12:04 -08:00

639 lines
20 KiB
C++

/*
* This program source code file is part of KiCad, a free EDA CAD application.
*
* Copyright The KiCad Developers, see AUTHORS.txt for contributors.
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, you may find one here:
* http://www.gnu.org/licenses/old-licenses/gpl-2.0.html
* or you may search the http://www.gnu.org website for the version 2 license,
* or you may write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
*/
#include <string_utils.h>
#include <scintilla_tricks.h>
#include <widgets/wx_grid.h>
#include <widgets/ui_common.h>
#include <wx/stc/stc.h>
#include <gal/color4d.h>
#include <dialog_shim.h>
#include <wx/clipbrd.h>
#include <wx/log.h>
#include <wx/settings.h>
#include <confirm.h>
SCINTILLA_TRICKS::SCINTILLA_TRICKS( wxStyledTextCtrl* aScintilla, const wxString& aBraces,
bool aSingleLine,
std::function<void( wxKeyEvent& )> onAcceptFn,
std::function<void( wxStyledTextEvent& )> onCharAddedFn ) :
m_te( aScintilla ),
m_braces( aBraces ),
m_lastCaretPos( -1 ),
m_lastSelStart( -1 ),
m_lastSelEnd( -1 ),
m_suppressAutocomplete( false ),
m_singleLine( aSingleLine ),
m_onAcceptFn( std::move( onAcceptFn ) ),
m_onCharAddedFn( std::move( onCharAddedFn ) )
{
// Always use LF as eol char, regardless the platform
m_te->SetEOLMode( wxSTC_EOL_LF );
// A hack which causes Scintilla to auto-size the text editor canvas
// See: https://github.com/jacobslusser/ScintillaNET/issues/216
m_te->SetScrollWidth( 1 );
m_te->SetScrollWidthTracking( true );
if( m_singleLine )
{
m_te->SetUseVerticalScrollBar( false );
m_te->SetUseHorizontalScrollBar( false );
}
setupStyles();
// Set up autocomplete
m_te->AutoCompSetIgnoreCase( true );
m_te->AutoCompSetMaxHeight( 20 );
if( aBraces.Length() >= 2 )
m_te->AutoCompSetFillUps( m_braces[1] );
// Hook up events
m_te->Bind( wxEVT_STC_UPDATEUI, &SCINTILLA_TRICKS::onScintillaUpdateUI, this );
m_te->Bind( wxEVT_STC_MODIFIED, &SCINTILLA_TRICKS::onModified, this );
// Handle autocomplete
m_te->Bind( wxEVT_STC_CHARADDED, &SCINTILLA_TRICKS::onChar, this );
m_te->Bind( wxEVT_STC_AUTOCOMP_CHAR_DELETED, &SCINTILLA_TRICKS::onChar, this );
// Dispatch command-keys in Scintilla control.
m_te->Bind( wxEVT_CHAR_HOOK, &SCINTILLA_TRICKS::onCharHook, this );
m_te->Bind( wxEVT_SYS_COLOUR_CHANGED,
wxSysColourChangedEventHandler( SCINTILLA_TRICKS::onThemeChanged ), this );
}
void SCINTILLA_TRICKS::onThemeChanged( wxSysColourChangedEvent &aEvent )
{
setupStyles();
aEvent.Skip();
}
void SCINTILLA_TRICKS::setupStyles()
{
wxTextCtrl dummy( m_te->GetParent(), wxID_ANY );
KIGFX::COLOR4D foreground = dummy.GetForegroundColour();
KIGFX::COLOR4D background = dummy.GetBackgroundColour();
KIGFX::COLOR4D highlight = wxSystemSettings::GetColour( wxSYS_COLOUR_HIGHLIGHT );
KIGFX::COLOR4D highlightText = wxSystemSettings::GetColour( wxSYS_COLOUR_HIGHLIGHTTEXT );
m_te->StyleSetForeground( wxSTC_STYLE_DEFAULT, foreground.ToColour() );
m_te->StyleSetBackground( wxSTC_STYLE_DEFAULT, background.ToColour() );
m_te->StyleClearAll();
// Scintilla doesn't handle alpha channel, which at least OSX uses in some highlight colours,
// such as "graphite".
highlight = highlight.Mix( background, highlight.a ).WithAlpha( 1.0 );
highlightText = highlightText.Mix( background, highlightText.a ).WithAlpha( 1.0 );
m_te->SetSelForeground( true, highlightText.ToColour() );
m_te->SetSelBackground( true, highlight.ToColour() );
m_te->SetCaretForeground( foreground.ToColour() );
if( !m_singleLine )
{
// Set a monospace font with a tab width of 4. This is the closest we can get to having
// Scintilla mimic the stroke font's tab positioning.
wxFont fixedFont = KIUI::GetMonospacedUIFont();
for( size_t i = 0; i < wxSTC_STYLE_MAX; ++i )
m_te->StyleSetFont( i, fixedFont );
m_te->SetTabWidth( 4 );
}
// Set up the brace highlighting. Scintilla doesn't handle alpha, so we construct our own
// 20% wash by blending with the background.
KIGFX::COLOR4D braceText = foreground;
KIGFX::COLOR4D braceHighlight = braceText.Mix( background, 0.2 );
m_te->StyleSetForeground( wxSTC_STYLE_BRACELIGHT, highlightText.ToColour() );
m_te->StyleSetBackground( wxSTC_STYLE_BRACELIGHT, braceHighlight.ToColour() );
m_te->StyleSetForeground( wxSTC_STYLE_BRACEBAD, *wxRED );
}
bool isCtrlSlash( wxKeyEvent& aEvent )
{
if( !aEvent.ControlDown() || aEvent.MetaDown() )
return false;
if( aEvent.GetUnicodeKey() == '/' )
return true;
// OK, now the wxWidgets hacks start.
// (We should abandon these if https://trac.wxwidgets.org/ticket/18911 gets resolved.)
// Many Latin America and European keyboars have have the / over the 7. We know that
// wxWidgets messes this up and returns Shift+7 through GetUnicodeKey(). However, other
// keyboards (such as France and Belgium) have 7 in the shifted position, so a Shift+7
// *could* be legitimate.
// However, we *are* checking Ctrl, so to assume any Shift+7 is a Ctrl-/ really only
// disallows Ctrl+Shift+7 from doing something else, which is probably OK. (This routine
// is only used in the Scintilla editor, not in the rest of Kicad.)
// The other main shifted loation of / is over : (France and Belgium), so we'll sacrifice
// Ctrl+Shift+: too.
if( aEvent.ShiftDown() && ( aEvent.GetUnicodeKey() == '7' || aEvent.GetUnicodeKey() == ':' ) )
return true;
// A few keyboards have / in an Alt position. Since we're expressly not checking Alt for
// up or down, those should work. However, if they don't, there's room below for yet
// another hack....
return false;
}
void SCINTILLA_TRICKS::onChar( wxStyledTextEvent& aEvent )
{
m_onCharAddedFn( aEvent );
}
void SCINTILLA_TRICKS::onModified( wxStyledTextEvent& aEvent )
{
if( m_singleLine )
{
wxString curr_text = m_te->GetText();
if( curr_text.Contains( wxS( "\n" ) ) || curr_text.Contains( wxS( "\r" ) ) )
{
// Scintilla won't allow us to call SetText() from within this event processor,
// so we have to delay the processing.
CallAfter( [this]()
{
wxString text = m_te->GetText();
int currpos = m_te->GetCurrentPos();
text.Replace( wxS( "\n" ), wxS( "" ) );
text.Replace( wxS( "\r" ), wxS( "" ) );
m_te->SetText( text );
m_te->GotoPos( currpos-1 );
} );
}
}
}
void SCINTILLA_TRICKS::onCharHook( wxKeyEvent& aEvent )
{
wxString c = aEvent.GetUnicodeKey();
if( m_te->AutoCompActive() )
{
if( aEvent.GetKeyCode() == WXK_ESCAPE )
{
m_te->AutoCompCancel();
m_suppressAutocomplete = true; // Don't run autocomplete again on the next char...
}
else if( aEvent.GetKeyCode() == WXK_RETURN || aEvent.GetKeyCode() == WXK_NUMPAD_ENTER )
{
int start = m_te->AutoCompPosStart();
m_te->AutoCompComplete();
int finish = m_te->GetCurrentPos();
if( finish > start )
{
// Select the last substitution token (if any) in the autocompleted text
int selStart = m_te->FindText( finish, start, "<" );
int selEnd = m_te->FindText( finish, start, ">" );
if( selStart > start && selEnd <= finish && selEnd > selStart )
m_te->SetSelection( selStart, selEnd + 1 );
}
}
else
{
aEvent.Skip();
}
return;
}
#ifdef __WXMAC__
if( aEvent.GetModifiers() == wxMOD_RAW_CONTROL && aEvent.GetKeyCode() == WXK_SPACE )
#else
if( aEvent.GetModifiers() == wxMOD_CONTROL && aEvent.GetKeyCode() == WXK_SPACE )
#endif
{
m_suppressAutocomplete = false;
wxStyledTextEvent event;
event.SetKey( ' ' );
event.SetModifiers( wxMOD_CONTROL );
m_onCharAddedFn( event );
return;
}
if( !isalpha( aEvent.GetKeyCode() ) )
m_suppressAutocomplete = false;
if( ( aEvent.GetKeyCode() == WXK_RETURN || aEvent.GetKeyCode() == WXK_NUMPAD_ENTER )
&& ( m_singleLine || aEvent.ShiftDown() ) )
{
m_onAcceptFn( aEvent );
}
else if( ConvertSmartQuotesAndDashes( &c ) )
{
m_te->AddText( c );
}
else if( aEvent.GetKeyCode() == WXK_TAB )
{
wxWindow* ancestor = m_te->GetParent();
while( ancestor && !dynamic_cast<WX_GRID*>( ancestor ) )
ancestor = ancestor->GetParent();
if( aEvent.ControlDown() )
{
int flags = 0;
if( !aEvent.ShiftDown() )
flags |= wxNavigationKeyEvent::IsForward;
if( DIALOG_SHIM* dlg = dynamic_cast<DIALOG_SHIM*>( wxGetTopLevelParent( m_te ) ) )
dlg->NavigateIn( flags );
}
else if( dynamic_cast<WX_GRID*>( ancestor ) )
{
WX_GRID* grid = static_cast<WX_GRID*>( ancestor );
int row = grid->GetGridCursorRow();
int col = grid->GetGridCursorCol();
if( aEvent.ShiftDown() )
{
if( col > 0 )
{
col--;
}
else if( row > 0 )
{
col = (int) grid->GetNumberCols() - 1;
if( row > 0 )
row--;
else
row = (int) grid->GetNumberRows() - 1;
}
}
else
{
if( col < (int) grid->GetNumberCols() - 1 )
{
col++;
}
else if( row < grid->GetNumberRows() - 1 )
{
col = 0;
if( row < grid->GetNumberRows() - 1 )
row++;
else
row = 0;
}
}
grid->SetGridCursor( row, col );
}
else
{
m_te->Tab();
}
}
else if( aEvent.GetModifiers() == wxMOD_CONTROL && aEvent.GetKeyCode() == 'Z' )
{
m_te->Undo();
}
else if( ( aEvent.GetModifiers() == wxMOD_SHIFT+wxMOD_CONTROL && aEvent.GetKeyCode() == 'Z' )
|| ( aEvent.GetModifiers() == wxMOD_CONTROL && aEvent.GetKeyCode() == 'Y' ) )
{
m_te->Redo();
}
else if( aEvent.GetModifiers() == wxMOD_CONTROL && aEvent.GetKeyCode() == 'A' )
{
m_te->SelectAll();
}
else if( aEvent.GetModifiers() == wxMOD_CONTROL && aEvent.GetKeyCode() == 'X' )
{
m_te->Cut();
if( wxTheClipboard->Open() )
{
wxTheClipboard->Flush(); // Allow data to be available after closing KiCad
wxTheClipboard->Close();
}
}
else if( aEvent.GetModifiers() == wxMOD_CONTROL && aEvent.GetKeyCode() == 'C' )
{
m_te->Copy();
if( wxTheClipboard->Open() )
{
wxTheClipboard->Flush(); // Allow data to be available after closing KiCad
wxTheClipboard->Close();
}
}
else if( aEvent.GetModifiers() == wxMOD_CONTROL && aEvent.GetKeyCode() == 'V' )
{
if( m_te->GetSelectionEnd() > m_te->GetSelectionStart() )
m_te->DeleteBack();
wxLogNull doNotLog; // disable logging of failed clipboard actions
if( wxTheClipboard->Open() )
{
if( wxTheClipboard->IsSupported( wxDF_TEXT ) ||
wxTheClipboard->IsSupported( wxDF_UNICODETEXT ) )
{
wxTextDataObject data;
wxString str;
wxTheClipboard->GetData( data );
str = data.GetText();
ConvertSmartQuotesAndDashes( &str );
if( m_singleLine )
{
str.Replace( wxS( "\n" ), wxEmptyString );
str.Replace( wxS( "\r" ), wxEmptyString );
}
m_te->BeginUndoAction();
m_te->AddText( str );
m_te->EndUndoAction();
}
wxTheClipboard->Close();
}
}
else if( aEvent.GetKeyCode() == WXK_BACK )
{
if( aEvent.GetModifiers() == wxMOD_CONTROL )
#ifdef __WXMAC__
m_te->HomeExtend();
else if( aEvent.GetModifiers() == wxMOD_ALT )
#endif
m_te->WordLeftExtend();
m_te->DeleteBack();
}
else if( aEvent.GetKeyCode() == WXK_DELETE )
{
if( m_te->GetSelectionEnd() == m_te->GetSelectionStart() )
m_te->CharRightExtend();
if( m_te->GetSelectionEnd() > m_te->GetSelectionStart() )
m_te->DeleteBack();
}
else if( isCtrlSlash( aEvent ) )
{
int startLine = m_te->LineFromPosition( m_te->GetSelectionStart() );
int endLine = m_te->LineFromPosition( m_te->GetSelectionEnd() );
bool comment = firstNonWhitespace( startLine ) != '#';
int whitespaceCount;
m_te->BeginUndoAction();
for( int ii = startLine; ii <= endLine; ++ii )
{
if( comment )
m_te->InsertText( m_te->PositionFromLine( ii ), wxT( "#" ) );
else if( firstNonWhitespace( ii, &whitespaceCount ) == '#' )
m_te->DeleteRange( m_te->PositionFromLine( ii ) + whitespaceCount, 1 );
}
m_te->SetSelection( m_te->PositionFromLine( startLine ),
m_te->PositionFromLine( endLine ) + m_te->GetLineLength( endLine ) );
m_te->EndUndoAction();
}
#ifdef __WXMAC__
else if( aEvent.GetModifiers() == wxMOD_RAW_CONTROL && aEvent.GetKeyCode() == 'A' )
{
m_te->HomeWrap();
}
else if( aEvent.GetModifiers() == wxMOD_RAW_CONTROL && aEvent.GetKeyCode() == 'E' )
{
m_te->LineEndWrap();
}
else if( ( aEvent.GetModifiers() & wxMOD_RAW_CONTROL ) && aEvent.GetKeyCode() == 'B' )
{
if( aEvent.GetModifiers() & wxMOD_ALT )
m_te->WordLeft();
else
m_te->CharLeft();
}
else if( ( aEvent.GetModifiers() & wxMOD_RAW_CONTROL ) && aEvent.GetKeyCode() == 'F' )
{
if( aEvent.GetModifiers() & wxMOD_ALT )
m_te->WordRight();
else
m_te->CharRight();
}
else if( aEvent.GetModifiers() == wxMOD_RAW_CONTROL && aEvent.GetKeyCode() == 'D' )
{
if( m_te->GetSelectionEnd() == m_te->GetSelectionStart() )
m_te->CharRightExtend();
if( m_te->GetSelectionEnd() > m_te->GetSelectionStart() )
m_te->DeleteBack();
}
#endif
else if( aEvent.GetKeyCode() == WXK_SPECIAL20 )
{
// Proxy for a wxSysColourChangedEvent
setupStyles();
}
else
{
aEvent.Skip();
}
}
int SCINTILLA_TRICKS::firstNonWhitespace( int aLine, int* aWhitespaceCharCount )
{
int lineStart = m_te->PositionFromLine( aLine );
if( aWhitespaceCharCount )
*aWhitespaceCharCount = 0;
for( int ii = 0; ii < m_te->GetLineLength( aLine ); ++ii )
{
int c = m_te->GetCharAt( lineStart + ii );
if( c == ' ' || c == '\t' )
{
if( aWhitespaceCharCount )
*aWhitespaceCharCount += 1;
continue;
}
else
{
return c;
}
}
return '\r';
}
void SCINTILLA_TRICKS::onScintillaUpdateUI( wxStyledTextEvent& aEvent )
{
auto isBrace = [this]( int c ) -> bool
{
return m_braces.Find( (wxChar) c ) >= 0;
};
// Has the caret changed position?
int caretPos = m_te->GetCurrentPos();
int selStart = m_te->GetSelectionStart();
int selEnd = m_te->GetSelectionEnd();
if( m_lastCaretPos != caretPos || m_lastSelStart != selStart || m_lastSelEnd != selEnd )
{
m_lastCaretPos = caretPos;
m_lastSelStart = selStart;
m_lastSelEnd = selEnd;
int bracePos1 = -1;
int bracePos2 = -1;
// Is there a brace to the left or right?
if( caretPos > 0 && isBrace( m_te->GetCharAt( caretPos-1 ) ) )
bracePos1 = ( caretPos - 1 );
else if( isBrace( m_te->GetCharAt( caretPos ) ) )
bracePos1 = caretPos;
if( bracePos1 >= 0 )
{
// Find the matching brace
bracePos2 = m_te->BraceMatch( bracePos1 );
if( bracePos2 == -1 )
{
m_te->BraceBadLight( bracePos1 );
m_te->SetHighlightGuide( 0 );
}
else
{
m_te->BraceHighlight( bracePos1, bracePos2 );
m_te->SetHighlightGuide( m_te->GetColumn( bracePos1 ) );
}
}
else
{
// Turn off brace matching
m_te->BraceHighlight( -1, -1 );
m_te->SetHighlightGuide( 0 );
}
}
}
void SCINTILLA_TRICKS::DoTextVarAutocomplete(
const std::function<void( const wxString& xRef, wxArrayString* tokens )>& getTokensFn )
{
wxArrayString autocompleteTokens;
int text_pos = m_te->GetCurrentPos();
int start = m_te->WordStartPosition( text_pos, true );
wxString partial;
auto textVarRef =
[&]( int pos )
{
return pos >= 2 && m_te->GetCharAt( pos-2 ) == '$' && m_te->GetCharAt( pos-1 ) == '{';
};
// Check for cross-reference
if( start > 1 && m_te->GetCharAt( start-1 ) == ':' )
{
int refStart = m_te->WordStartPosition( start-1, true );
if( textVarRef( refStart ) )
{
partial = m_te->GetRange( start, text_pos );
getTokensFn( m_te->GetRange( refStart, start-1 ), &autocompleteTokens );
}
}
else if( textVarRef( start ) )
{
partial = m_te->GetTextRange( start, text_pos );
getTokensFn( wxEmptyString, &autocompleteTokens );
}
DoAutocomplete( partial, autocompleteTokens );
m_te->SetFocus();
}
void SCINTILLA_TRICKS::DoAutocomplete( const wxString& aPartial, const wxArrayString& aTokens )
{
if( m_suppressAutocomplete )
return;
wxArrayString matchedTokens;
wxString filter = wxT( "*" ) + aPartial.Lower() + wxT( "*" );
for( const wxString& token : aTokens )
{
if( token.Lower().Matches( filter ) )
matchedTokens.push_back( token );
}
if( matchedTokens.size() > 0 )
{
// NB: tokens MUST be in alphabetical order because the Scintilla engine is going
// to do a binary search on them
matchedTokens.Sort( []( const wxString& first, const wxString& second ) -> int
{
return first.CmpNoCase( second );
});
m_te->AutoCompSetSeparator( '\t' );
m_te->AutoCompShow( aPartial.size(), wxJoin( matchedTokens, '\t' ) );
}
}
void SCINTILLA_TRICKS::CancelAutocomplete()
{
m_te->AutoCompCancel();
}