/*
* 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 3 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, see .
*/
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
// The "official" name of the building Kicad stroke font (always existing)
#include
class FONT_LIST_MANAGER : public wxEvtHandler
{
public:
static FONT_LIST_MANAGER& Get();
wxArrayString GetFonts() const;
void Register( FONT_CHOICE* aCtrl );
void Unregister( FONT_CHOICE* aCtrl );
private:
FONT_LIST_MANAGER();
~FONT_LIST_MANAGER();
void Poll();
void UpdateFonts();
std::thread m_thread;
mutable std::mutex m_mutex;
std::condition_variable m_cv;
wxArrayString m_fonts;
std::vector m_controls;
std::atomic m_quit;
};
FONT_LIST_MANAGER& FONT_LIST_MANAGER::Get()
{
static FONT_LIST_MANAGER mgr;
return mgr;
}
wxArrayString FONT_LIST_MANAGER::GetFonts() const
{
std::lock_guard lock( m_mutex );
return m_fonts;
}
void FONT_LIST_MANAGER::Register( FONT_CHOICE* aCtrl )
{
std::lock_guard lock( m_mutex );
m_controls.push_back( aCtrl );
}
void FONT_LIST_MANAGER::Unregister( FONT_CHOICE* aCtrl )
{
std::lock_guard lock( m_mutex );
auto it = std::find( m_controls.begin(), m_controls.end(), aCtrl );
if( it != m_controls.end() )
m_controls.erase( it );
}
FONT_LIST_MANAGER::FONT_LIST_MANAGER()
{
m_quit = false;
UpdateFonts();
// It appears that the polling mechanism does not work correctly
// for mingw (hangs on exit)
#ifndef __MINGW32__
m_thread = std::thread( &FONT_LIST_MANAGER::Poll, this );
#endif
}
FONT_LIST_MANAGER::~FONT_LIST_MANAGER()
{
{
std::lock_guard lock( m_mutex );
m_quit = true;
}
#ifndef __MINGW32__
m_cv.notify_one();
if( m_thread.joinable() )
m_thread.join();
#endif
}
void FONT_LIST_MANAGER::Poll()
{
std::unique_lock lock( m_mutex );
while( !m_quit )
{
// N.B. wait_for will unlock the mutex while waiting but lock it before continuing
// so we need to relock before continuing in the loop
m_cv.wait_for( lock, std::chrono::seconds( 30 ), [&] { return m_quit.load(); } );
if( !m_quit )
{
lock.unlock();
UpdateFonts();
lock.lock();
}
}
}
void FONT_LIST_MANAGER::UpdateFonts()
{
std::vector fontNames;
Fontconfig()->ListFonts( fontNames, std::string( Pgm().GetLanguageTag().utf8_str() ) );
wxArrayString menuList;
for( const std::string& name : fontNames )
menuList.Add( wxString( name ) );
menuList.Sort();
// Check if fonts changed and update controls
{
std::lock_guard lock( m_mutex );
if( menuList == m_fonts )
return;
m_fonts = menuList;
}
CallAfter( [this]() {
std::vector controlsCopy;
// Copy controls list under lock protection
{
std::lock_guard lock( m_mutex );
controlsCopy = m_controls;
}
// Update controls without holding lock
for( FONT_CHOICE* ctrl : controlsCopy )
{
if( ctrl && !ctrl->IsShownOnScreen() )
ctrl->RefreshFonts();
}
} );
}
FONT_CHOICE::FONT_CHOICE( wxWindow* aParent, int aId, wxPoint aPosition, wxSize aSize,
int nChoices, wxString* aChoices, int aStyle ) :
wxOwnerDrawnComboBox( aParent, aId, wxEmptyString, aPosition, aSize, 0, nullptr, aStyle )
{
m_systemFontCount = nChoices;
m_notFound = wxS( " " ) + _( "" );
m_isFiltered = false;
m_lastText = wxEmptyString;
m_originalSelection = wxEmptyString;
FONT_LIST_MANAGER::Get().Register( this );
RefreshFonts();
// Bind only essential events to restore functionality
Bind( wxEVT_KEY_DOWN, &FONT_CHOICE::OnKeyDown, this );
Bind( wxEVT_CHAR_HOOK, &FONT_CHOICE::OnCharHook, this );
Bind( wxEVT_COMMAND_TEXT_UPDATED, &FONT_CHOICE::OnTextCtrl, this );
Bind( wxEVT_COMBOBOX_DROPDOWN, &FONT_CHOICE::OnDropDown, this );
Bind( wxEVT_COMBOBOX_CLOSEUP, &FONT_CHOICE::OnCloseUp, this );
Bind( wxEVT_SET_FOCUS, &FONT_CHOICE::OnSetFocus, this );
Bind( wxEVT_KILL_FOCUS, &FONT_CHOICE::OnKillFocus, this );
}
FONT_CHOICE::~FONT_CHOICE()
{
FONT_LIST_MANAGER::Get().Unregister( this );
}
void FONT_CHOICE::RefreshFonts()
{
wxArrayString menuList = FONT_LIST_MANAGER::Get().GetFonts();
wxString selection = GetValue();
// Store the full font list for filtering
m_fullFontList.Clear();
if( m_systemFontCount > 1 )
m_fullFontList.Add( _( "Default Font" ) );
m_fullFontList.Add( KICAD_FONT_NAME );
for( const wxString& font : menuList )
m_fullFontList.Add( font );
Freeze();
Clear();
if( m_systemFontCount > 1 )
Append( _( "Default Font" ) );
Append( KICAD_FONT_NAME );
m_systemFontCount = GetCount();
Append( menuList );
if( !selection.IsEmpty() )
SetStringSelection( selection );
m_isFiltered = false;
Thaw();
}
void FONT_CHOICE::SetFontSelection( KIFONT::FONT* aFont, bool aSilentMode )
{
if( !aFont )
{
SetSelection( 0 );
}
else
{
bool result = SetStringSelection( aFont->GetName() );
if( !result )
{
Append( aFont->GetName() + m_notFound );
SetSelection( GetCount() - 1 );
}
}
}
bool FONT_CHOICE::HaveFontSelection() const
{
int sel = GetSelection();
if( sel < 0 )
return false;
if( GetString( sel ).EndsWith( m_notFound ) )
return false;
return true;
}
KIFONT::FONT* FONT_CHOICE::GetFontSelection( bool aBold, bool aItalic, bool aForDrawingSheet ) const
{
if( GetSelection() <= 0 )
{
return nullptr;
}
else if( GetSelection() == 1 && m_systemFontCount == 2 )
{
return KIFONT::FONT::GetFont( KICAD_FONT_NAME, aBold, aItalic );
}
else
{
return KIFONT::FONT::GetFont( GetValue(), aBold, aItalic, nullptr,
aForDrawingSheet );
}
}
void FONT_CHOICE::OnDrawItem( wxDC& dc, const wxRect& rect, int item, int flags ) const
{
if( item == wxNOT_FOUND )
return;
wxString name = GetString( item );
dc.SetFont( wxSystemSettings::GetFont( wxSYS_DEFAULT_GUI_FONT ) );
dc.DrawText( name, rect.x + 2, rect.y + 2 );
if( item >= m_systemFontCount )
{
wxCoord w, h;
dc.GetTextExtent( name, &w, &h );
wxFont sampleFont( wxFontInfo( dc.GetFont().GetPointSize() ).FaceName( name ) );
dc.SetFont( sampleFont );
dc.SetTextForeground( wxSystemSettings::GetColour( wxSYS_COLOUR_GRAYTEXT ) );
dc.DrawText( wxS( "AaBbCcDd123456" ), rect.x + w + 15, rect.y + 2 );
}
}
wxString FONT_CHOICE::GetStringSelection() const
{
return GetValue();
}
void FONT_CHOICE::OnKeyDown( wxKeyEvent& aEvent )
{
int keyCode = aEvent.GetKeyCode();
if( keyCode == WXK_RETURN || keyCode == WXK_NUMPAD_ENTER || keyCode == WXK_ESCAPE )
{
if( IsPopupShown() )
{
// Accept current text and close popup
Dismiss();
return;
}
}
else if( keyCode == WXK_BACK && !IsPopupShown() )
{
// Handle backspace when popup is not shown
// This allows normal character-by-character deletion instead of selecting all text
wxString currentText = GetValue();
long selStart, selEnd;
GetSelection( &selStart, &selEnd );
if( selStart != selEnd )
{
// There's a selection - delete the selected text
wxString newText = currentText.Left( selStart ) + currentText.Mid( selEnd );
m_lastText = newText; // Prevent recursive calls
ChangeValue( newText );
SetInsertionPoint( selStart );
return; // Don't skip this event
}
else if( selStart > 0 )
{
// No selection - delete character before cursor
wxString newText = currentText.Left( selStart - 1 ) + currentText.Mid( selStart );
m_lastText = newText; // Prevent recursive calls
ChangeValue( newText );
SetInsertionPoint( selStart - 1 );
return; // Don't skip this event
}
// If at beginning of text, let default behavior handle it
}
aEvent.Skip();
}
void FONT_CHOICE::OnCharHook( wxKeyEvent& aEvent )
{
int keyCode = aEvent.GetUnicodeKey();
wchar_t wc = static_cast( keyCode );
// When popup is not shown, let normal text processing handle printable characters
// The OnTextCtrl method will handle filtering and autocomplete
if( !IsPopupShown() )
{
aEvent.Skip();
return;
}
if( std::iswprint( wc ) && !std::iswcntrl( wc ) )
{
// Get current text and check if there's a selection
wxString currentText = GetValue();
long selStart, selEnd;
GetSelection( &selStart, &selEnd );
wxChar newChar = (wxChar)keyCode;
wxString newText;
if( selStart != selEnd )
{
// There's a selection - replace it with the new character
newText = currentText.Left( selStart ) + newChar + currentText.Mid( selEnd );
}
else
{
// No selection - append to current insertion point
long insertionPoint = GetInsertionPoint();
newText = currentText.Left( insertionPoint ) + newChar + currentText.Mid( insertionPoint );
}
// Update the text control
m_lastText = newText; // Prevent recursive calls
ChangeValue( newText );
SetInsertionPoint( selStart + 1 ); // Position cursor after new character
// Filter the font list based on new text (will handle trimming internally)
FilterFontList( newText );
// Try autocomplete
DoAutoComplete( newText );
return; // Don't skip this event
}
switch (keyCode)
{
case WXK_BACK:
{
wxString currentText = GetValue();
long selStart, selEnd;
GetSelection( &selStart, &selEnd );
wxString newText;
long newInsertionPoint;
if( selStart != selEnd )
{
// There's a selection - delete the selected text
newText = currentText.Left( selStart ) + currentText.Mid( selEnd );
newInsertionPoint = selStart;
}
else if( selStart > 0 )
{
// No selection - delete character before cursor
newText = currentText.Left( selStart - 1 ) + currentText.Mid( selStart );
newInsertionPoint = selStart - 1;
}
else
{
return; // At beginning, can't delete
}
m_lastText = newText; // Prevent recursive calls
ChangeValue( newText );
SetInsertionPoint( newInsertionPoint );
// Check if trimmed text is empty
wxString trimmedNewText = newText;
trimmedNewText.Trim().Trim( false );
if( trimmedNewText.IsEmpty() )
{
RestoreFullFontList();
}
else
{
FilterFontList( newText );
// Don't call DoAutoComplete for backspace to avoid the loop
}
return; // Don't skip this event
}
case WXK_RETURN:
case WXK_NUMPAD_ENTER:
{
Dismiss();
return;
}
break;
case WXK_ESCAPE:
{
// Restore to original selection or default font if original doesn't exist
if( !m_originalSelection.IsEmpty() && FindBestMatch( m_originalSelection ) != wxNOT_FOUND )
{
SetStringSelection( m_originalSelection );
m_lastText = m_originalSelection;
}
else
{
// Original font doesn't exist anymore, select default font
wxString defaultFont = GetDefaultFontName();
SetStringSelection( defaultFont );
m_lastText = defaultFont;
}
// Restore full font list if filtered
if( m_isFiltered )
{
RestoreFullFontList();
}
// Only dismiss if popup is actually shown
if( IsPopupShown() )
{
Dismiss();
}
return;
}
default:
break;
}
aEvent.Skip();
}
void FONT_CHOICE::OnTextCtrl( wxCommandEvent& aEvent )
{
wxString currentText = GetValue();
// Avoid recursive calls
if( currentText == m_lastText )
{
aEvent.Skip();
return;
}
m_lastText = currentText;
// If popup is shown, OnCharHook handles the text input, so just skip
if( IsPopupShown() )
{
aEvent.Skip();
return;
}
// Trim whitespace for processing
wxString trimmedText = currentText;
trimmedText.Trim().Trim(false);
// If text is empty or all whitespace, restore full list
if( trimmedText.IsEmpty() )
{
RestoreFullFontList();
aEvent.Skip();
return;
}
// Filter the font list based on the text input
FilterFontList( currentText );
// Try to find a match for autocomplete (only when popup is not shown)
int bestMatch = FindBestMatch( trimmedText );
if( bestMatch != wxNOT_FOUND )
{
DoAutoComplete( trimmedText );
}
aEvent.Skip();
}
void FONT_CHOICE::OnDropDown( wxCommandEvent& aEvent )
{
// Store the original selection when dropdown opens
m_originalSelection = GetValue();
aEvent.Skip();
}
void FONT_CHOICE::OnCloseUp( wxCommandEvent& aEvent )
{
// When dropdown closes, we should only restore the full font list
// but NOT change the current text value unless explicitly selected
// The OnKillFocus handler will handle text validation when focus is lost
// Reset to full font list if filtered
if( m_isFiltered )
{
RestoreFullFontList();
}
aEvent.Skip();
}
void FONT_CHOICE::OnSetFocus( wxFocusEvent& aEvent )
{
// When the control gains focus, select all text so user can quickly replace it
// Only do this if we're not already showing the popup (which would indicate
// the user is actively interacting with the dropdown)
if( !GetValue().IsEmpty() && !IsPopupShown() )
{
// Use CallAfter to ensure the focus event is fully processed first
CallAfter( [this]() {
if( HasFocus() && !IsPopupShown() )
SelectAll();
} );
}
aEvent.Skip();
}
void FONT_CHOICE::OnKillFocus( wxFocusEvent& aEvent )
{
// When losing focus, deselect text and validate/correct the font name
// First, deselect any selected text
if( GetInsertionPoint() != GetLastPosition() )
{
SetInsertionPointEnd();
}
// Get current text and trim whitespace
wxString currentText = GetValue();
currentText.Trim().Trim(false);
// If text is empty, set to default font
if( currentText.IsEmpty() )
{
wxString defaultFont = GetDefaultFontName();
SetStringSelection( defaultFont );
m_lastText = defaultFont;
aEvent.Skip();
return;
}
// Try to find exact match first
if( FindBestMatch( currentText ) != wxNOT_FOUND )
{
// Exact match found, keep the current text but ensure it's properly set
SetStringSelection( currentText );
m_lastText = currentText;
aEvent.Skip();
return;
}
// No exact match, try to find best partial match
wxString partialMatch = FindBestPartialMatch( currentText );
if( !partialMatch.IsEmpty() )
{
SetStringSelection( partialMatch );
m_lastText = partialMatch;
}
else
{
// No decent partial match, fall back to default font
wxString defaultFont = GetDefaultFontName();
SetStringSelection( defaultFont );
m_lastText = defaultFont;
}
// Ensure we restore full font list if it was filtered
if( m_isFiltered )
{
RestoreFullFontList();
}
aEvent.Skip();
}
void FONT_CHOICE::DoAutoComplete( const wxString& aText )
{
if( aText.IsEmpty() )
return;
// Find the best matching font
int bestMatch = FindBestMatch( aText );
if( bestMatch == wxNOT_FOUND )
return;
wxString matchText = GetString( bestMatch );
// Only do autocomplete if the match is longer than what we typed
if( matchText.Length() > aText.Length() && matchText.Lower().StartsWith( aText.Lower() ) )
{
// Set the text with the autocompleted portion selected
m_lastText = matchText; // Update to prevent recursive calls
ChangeValue( matchText );
SetInsertionPoint( aText.Length() );
SetSelection( aText.Length(), matchText.Length() );
if( IsPopupShown() )
{
SetSelection( bestMatch );
}
}
}
void FONT_CHOICE::FilterFontList( const wxString& aFilter )
{
// Trim whitespace from filter
wxString trimmedFilter = aFilter;
trimmedFilter.Trim().Trim(false);
if( trimmedFilter.IsEmpty() )
{
RestoreFullFontList();
return;
}
wxArrayString filteredList;
// Add system fonts first
for( int i = 0; i < m_systemFontCount; i++ )
{
wxString fontName = m_fullFontList[i];
if( fontName.Lower().StartsWith( trimmedFilter.Lower() ) )
filteredList.Add( fontName );
}
// Add matching fonts from the full list
for( size_t i = m_systemFontCount; i < m_fullFontList.GetCount(); i++ )
{
wxString fontName = m_fullFontList[i];
if( fontName.Lower().StartsWith( trimmedFilter.Lower() ) )
filteredList.Add( fontName );
}
// Preserve the current text value
wxString currentText = GetValue();
// Check if we had items before and now have none - this indicates we need to force refresh
bool hadItemsBefore = GetCount() > 0;
bool haveItemsNow = filteredList.GetCount() > 0;
bool needsPopupRefresh = hadItemsBefore && !haveItemsNow && IsPopupShown();
// Update the combo box with filtered list (even if empty)
Freeze();
Clear();
if( haveItemsNow )
{
Append( filteredList );
}
// If no matches, leave the dropdown empty
m_isFiltered = true;
// Restore the text value after filtering
if( !currentText.IsEmpty() )
{
ChangeValue( currentText );
SetInsertionPointEnd();
}
Thaw();
// Handle popup display
if( needsPopupRefresh )
{
// We had items before but now have none - dismiss the popup
Dismiss();
}
else if( !IsPopupShown() && haveItemsNow )
{
// Only show popup if we have items to display and control has focus
// This prevents popup from showing during programmatic text changes
if( HasFocus() )
{
Popup();
}
}
else if( IsPopupShown() && !haveItemsNow )
{
// If popup is shown but we have no items, dismiss it
Dismiss();
}
// Force a refresh to ensure the popup displays correctly only if it's shown and has items
if( IsPopupShown() && haveItemsNow )
{
Update();
Refresh();
}
}
void FONT_CHOICE::RestoreFullFontList()
{
if( !m_isFiltered )
return;
wxString selection = GetValue();
Freeze();
Clear();
Append( m_fullFontList );
m_isFiltered = false;
if( !selection.IsEmpty() )
{
ChangeValue( selection );
SetInsertionPointEnd();
}
Thaw();
}
int FONT_CHOICE::FindBestMatch( const wxString& aText )
{
if( aText.IsEmpty() )
return wxNOT_FOUND;
// Trim whitespace from search text
wxString trimmedText = aText;
trimmedText.Trim().Trim(false);
if( trimmedText.IsEmpty() )
return wxNOT_FOUND;
wxString lowerText = trimmedText.Lower();
// Search in the full font list to find matches, then map to current list
for( size_t i = 0; i < m_fullFontList.GetCount(); i++ )
{
wxString itemText = m_fullFontList[i].Lower();
if( itemText.StartsWith( lowerText ) )
{
// Find this font in the current displayed list
wxString fullFontName = m_fullFontList[i];
for( unsigned int j = 0; j < GetCount(); j++ )
{
if( GetString( j ) == fullFontName )
return j;
}
}
}
return wxNOT_FOUND;
}
wxString FONT_CHOICE::FindBestPartialMatch( const wxString& aText )
{
if( aText.IsEmpty() )
return wxEmptyString;
// Trim whitespace from search text
wxString trimmedText = aText;
trimmedText.Trim().Trim(false);
if( trimmedText.IsEmpty() )
return wxEmptyString;
wxString testText = trimmedText;
// Try progressively shorter versions of the text by removing characters from the end
// Don't go below a minimum length of 2 characters for meaningful partial matches
while( testText.Length() >= 2 )
{
wxString lowerTestText = testText.Lower();
// Search in the full font list for a match
for( size_t i = 0; i < m_fullFontList.GetCount(); i++ )
{
wxString itemText = m_fullFontList[i].Lower();
if( itemText.StartsWith( lowerTestText ) )
{
// Found a match, return the full font name
return m_fullFontList[i];
}
}
// Remove the last character and try again
testText = testText.Left( testText.Length() - 1 );
}
// No decent partial match found (need at least 2 characters for a meaningful match)
return wxEmptyString;
}
wxString FONT_CHOICE::GetDefaultFontName() const
{
// Return KiCad font name as the default
return KICAD_FONT_NAME;
}