/* * 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; }