KiCad PCB EDA Suite
Loading...
Searching...
No Matches
filter_combobox.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 The KiCad Developers, see AUTHORS.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, see <https://www.gnu.org/licenses/>.
18 */
19
20
22
23#include <wx/textctrl.h>
24#include <wx/listbox.h>
25#include <wx/settings.h>
26#include <wx/sizer.h>
27#include <wx/stattext.h>
28#include <wx/valtext.h>
29
30#include <kiplatform/ui.h>
31#include <widgets/ui_common.h>
32
33wxDEFINE_EVENT( FILTERED_ITEM_SELECTED, wxCommandEvent );
34
35#if defined( __WXOSX_MAC__ )
36 #define POPUP_PADDING 2
37 #define LIST_ITEM_PADDING 2
38 #define LIST_PADDING 7
39#elif defined( __WXMSW__ )
40 #define POPUP_PADDING 0
41 #define LIST_ITEM_PADDING 2
42 #define LIST_PADDING 5
43#else
44 #define POPUP_PADDING 0
45 #define LIST_ITEM_PADDING 6
46 #define LIST_PADDING 5
47#endif
48
49
51 m_filterValidator( nullptr ),
52 m_filterCtrl( nullptr ),
53 m_listBox( nullptr ),
54 m_minPopupWidth( -1 ),
55 m_maxPopupHeight( 1000 ),
56 m_focusHandler( nullptr )
57{
58}
59
60
61bool FILTER_COMBOPOPUP::Create( wxWindow* aParent )
62{
63 wxPanel::Create( aParent, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxSIMPLE_BORDER );
64
65 wxBoxSizer* mainSizer;
66 mainSizer = new wxBoxSizer( wxVERTICAL );
67
68 wxStaticText* filterLabel = new wxStaticText( this, wxID_ANY, _( "Filter:" ) );
69 mainSizer->Add( filterLabel, 0, wxEXPAND, 0 );
70
71 m_filterCtrl = new wxTextCtrl( this, wxID_ANY, wxEmptyString, wxDefaultPosition, wxDefaultSize,
72 wxTE_PROCESS_ENTER );
73 m_filterValidator = new wxTextValidator( wxFILTER_EXCLUDE_CHAR_LIST );
74 m_filterValidator->SetCharExcludes( " " );
75 m_filterCtrl->SetValidator( *m_filterValidator );
76 mainSizer->Add( m_filterCtrl, 0, wxEXPAND, 0 );
77
78 m_listBox = new wxListBox( this, wxID_ANY, wxDefaultPosition, wxDefaultSize, 0, nullptr,
79 wxLB_SINGLE | wxLB_NEEDED_SB );
80 mainSizer->Add( m_listBox, 1, wxEXPAND | wxTOP, 2 );
81
82 SetSizer( mainSizer );
83 Layout();
84
85 Connect( wxEVT_IDLE, wxIdleEventHandler( FILTER_COMBOPOPUP::onIdle ), nullptr, this );
86 Connect( wxEVT_CHAR_HOOK, wxKeyEventHandler( FILTER_COMBOPOPUP::onKeyDown ), nullptr, this );
87 Connect( wxEVT_LEFT_DOWN, wxMouseEventHandler( FILTER_COMBOPOPUP::onMouseClick ), nullptr, this );
88 m_listBox->Connect( wxEVT_LEFT_DOWN, wxMouseEventHandler( FILTER_COMBOPOPUP::onMouseClick ), nullptr, this );
89 m_filterCtrl->Connect( wxEVT_TEXT, wxCommandEventHandler( FILTER_COMBOPOPUP::onFilterEdit ), nullptr, this );
90 m_filterCtrl->Connect( wxEVT_TEXT_ENTER, wxCommandEventHandler( FILTER_COMBOPOPUP::onEnter ), nullptr, this );
91
92 // <enter> in a ListBox comes in as a double-click on GTK
93 m_listBox->Connect( wxEVT_COMMAND_LISTBOX_DOUBLECLICKED, wxCommandEventHandler( FILTER_COMBOPOPUP::onEnter ),
94 nullptr, this );
95
96 return true;
97}
98
99
100void FILTER_COMBOPOPUP::SetStringList( const wxArrayString& aStringList )
101{
102 m_stringList = aStringList;
103 m_stringList.Sort();
104 rebuildList();
105}
106
107
109{
110 return m_selectedString;
111}
112
113
114void FILTER_COMBOPOPUP::SetStringValue( const wxString& aNetName )
115{
116 if( GetWindowStyleFlag() & wxCB_READONLY )
117 /* shouldn't be here (combo is read-only) */;
118 else
119 wxComboPopup::SetStringValue( aNetName );
120}
121
122
123void FILTER_COMBOPOPUP::SetSelectedString( const wxString& aString )
124{
125 m_selectedString = aString;
126 GetComboCtrl()->SetValue( m_selectedString );
127}
128
129
131{
132 // While it can sometimes be useful to keep the filter, it's always unexpected.
133 // Better to clear it.
134 m_filterCtrl->Clear();
135
136 m_listBox->SetStringSelection( GetStringValue() );
137 m_filterCtrl->SetFocus();
138
139 // The updateSize() call in GetAdjustedSize() leaves the height off-by-one for
140 // some reason, so do it again.
141 updateSize();
142}
143
144
145void FILTER_COMBOPOPUP::OnStartingKey( wxKeyEvent& aEvent )
146{
148 doStartingKey( aEvent );
149}
150
151
153{
154 wxString selectedString = getSelectedValue().value_or( wxEmptyString );
155
156 Dismiss();
157
158 // No update on empty
159 if( !selectedString.IsEmpty() && selectedString != m_selectedString )
160 {
161 m_selectedString = selectedString;
162 GetComboCtrl()->SetValue( m_selectedString );
163
164 wxCommandEvent changeEvent( FILTERED_ITEM_SELECTED );
165 wxPostEvent( GetComboCtrl(), changeEvent );
166 }
167}
168
169
170wxSize FILTER_COMBOPOPUP::GetAdjustedSize( int aMinWidth, int aPrefHeight, int aMaxHeight )
171{
172 // Called when the popup is first shown. Stash the minWidth and maxHeight so we
173 // can use them later when refreshing the sizes after filter changes.
174 m_minPopupWidth = aMinWidth;
175 m_maxPopupHeight = aMaxHeight;
176
177 return updateSize();
178}
179
180
181void FILTER_COMBOPOPUP::getListContent( wxArrayString& aListContent )
182{
183 const wxString filterString = getFilterValue();
184
185 // Simple substring, case-insensitive search
186 for( const wxString& str : m_stringList )
187 {
188 if( filterString.IsEmpty() || str.Lower().Contains( filterString.Lower() ) )
189 aListContent.push_back( str );
190 }
191}
192
193
195{
196 wxArrayString newList;
197 getListContent( newList );
198
199 m_listBox->Set( newList );
200}
201
202
203std::optional<wxString> FILTER_COMBOPOPUP::getSelectedValue() const
204{
205 int selection = m_listBox->GetSelection();
206
207 if( selection >= 0 )
208 return m_listBox->GetString( selection );
209
210 return std::nullopt;
211}
212
213
215{
216 return m_filterCtrl->GetValue().Trim( true ).Trim( false );
217}
218
219
221{
222 int listTop = m_listBox->GetRect().y;
223 int itemHeight = KIUI::GetTextSize( wxT( "Xy" ), this ).y + LIST_ITEM_PADDING;
224 int listHeight = ( (int) m_listBox->GetCount() * itemHeight ) + ( LIST_PADDING * 3 );
225
226 if( listTop + listHeight >= m_maxPopupHeight )
227 listHeight = m_maxPopupHeight - listTop - 1;
228
229 int listWidth = m_minPopupWidth;
230
231 for( size_t i = 0; i < m_listBox->GetCount(); ++i )
232 {
233 int itemWidth = KIUI::GetTextSize( m_listBox->GetString( i ), m_listBox ).x;
234 listWidth = std::max( listWidth, itemWidth + LIST_PADDING * 2 );
235 }
236
237 wxSize listSize( listWidth, listHeight );
238 wxSize popupSize( listWidth, listTop + listHeight );
239
240 SetSize( popupSize ); // us
241 GetParent()->SetSize( popupSize ); // the window that wxComboCtrl put us in
242
243 m_listBox->SetMinSize( listSize );
244 m_listBox->SetSize( listSize );
245
246 return popupSize;
247}
248
249
250void FILTER_COMBOPOPUP::onIdle( wxIdleEvent& aEvent )
251{
252 // Only process when the popup is actually visible to avoid ClientToScreen warnings.
253 // Use IsShownOnScreen() instead of IsShown() because wx may report the window as shown
254 // before GTK has fully realized it, causing ClientToScreen to fail.
255 if( !IsShownOnScreen() )
256 return;
257
258 // Generate synthetic (but reliable) MouseMoved events
259 static wxPoint lastPos;
260 wxPoint screenPos = KIPLATFORM::UI::GetMousePosition();
261
262 if( screenPos != lastPos )
263 {
264 lastPos = screenPos;
265 onMouseMoved( screenPos );
266 }
267
268 if( m_focusHandler )
269 {
270 m_filterCtrl->PushEventHandler( m_focusHandler );
271 m_focusHandler = nullptr;
272 }
273}
274
275
276// Hot-track the mouse (for focus and listbox selection)
277void FILTER_COMBOPOPUP::onMouseMoved( const wxPoint aScreenPos )
278{
279 if( m_listBox->GetScreenRect().Contains( aScreenPos ) )
280 {
282
283 wxPoint relativePos = m_listBox->ScreenToClient( aScreenPos );
284 int item = m_listBox->HitTest( relativePos );
285
286 if( item >= 0 )
287 m_listBox->SetSelection( item );
288 }
289 else if( m_filterCtrl->GetScreenRect().Contains( aScreenPos ) )
290 {
292 }
293}
294
295
296void FILTER_COMBOPOPUP::onMouseClick( wxMouseEvent& aEvent )
297{
298 // Accept a click event from anywhere. Different platform implementations have
299 // different foibles with regard to transient popups and their children.
300 if( aEvent.GetEventObject() == m_listBox )
301 {
302 m_listBox->SetSelection( m_listBox->HitTest( aEvent.GetPosition() ) );
303 Accept();
304 return;
305 }
306
307 wxWindow* window = dynamic_cast<wxWindow*>( aEvent.GetEventObject() );
308
309 if( window )
310 {
311 wxPoint screenPos = window->ClientToScreen( aEvent.GetPosition() );
312
313 if( m_listBox->GetScreenRect().Contains( screenPos ) )
314 {
315 wxPoint localPos = m_listBox->ScreenToClient( screenPos );
316
317 m_listBox->SetSelection( m_listBox->HitTest( localPos ) );
318 Accept();
319 }
320 }
321}
322
323
324void FILTER_COMBOPOPUP::onKeyDown( wxKeyEvent& aEvent )
325{
326 switch( aEvent.GetKeyCode() )
327 {
328 // Control keys go to the parent combobox
329 case WXK_TAB:
330 Dismiss();
331
332 m_parent->NavigateIn( ( aEvent.ShiftDown() ? 0 : wxNavigationKeyEvent::IsForward ) |
333 ( aEvent.ControlDown() ? wxNavigationKeyEvent::WinChange : 0 ) );
334 break;
335
336 case WXK_ESCAPE:
337 Dismiss();
338 break;
339
340 case WXK_RETURN:
341 case WXK_NUMPAD_ENTER:
342 Accept();
343 break;
344
345 // Arrows go to the list box
346 case WXK_DOWN:
347 case WXK_NUMPAD_DOWN:
349 m_listBox->SetSelection( std::min( m_listBox->GetSelection() + 1,
350 (int) m_listBox->GetCount() - 1 ) );
351 break;
352
353 case WXK_UP:
354 case WXK_NUMPAD_UP:
356 m_listBox->SetSelection( std::max( m_listBox->GetSelection() - 1, 0 ) );
357 break;
358
359 // Everything else goes to the filter textbox
360 default:
361 if( !m_filterCtrl->HasFocus() )
362 {
364
365 // Because we didn't have focus we missed our chance to have the native widget
366 // handle the keystroke. We'll have to do the first character ourselves.
367 doStartingKey( aEvent );
368 }
369 else
370 {
371 // On some platforms a wxComboFocusHandler will have been pushed which
372 // unhelpfully gives the event right back to the popup. Make sure the filter
373 // control is going to get the event.
374 if( m_filterCtrl->GetEventHandler() != m_filterCtrl )
375 m_focusHandler = m_filterCtrl->PopEventHandler();
376
377 aEvent.Skip();
378 }
379 break;
380 }
381}
382
383
384void FILTER_COMBOPOPUP::onEnter( wxCommandEvent& aEvent )
385{
386 Accept();
387}
388
389
390void FILTER_COMBOPOPUP::onFilterEdit( wxCommandEvent& aEvent )
391{
392 rebuildList();
393 updateSize();
394
395 if( m_listBox->GetCount() > 0 )
396 m_listBox->SetSelection( 0 );
397}
398
399
400void FILTER_COMBOPOPUP::doStartingKey( wxKeyEvent& aEvent )
401{
402 if( aEvent.GetKeyCode() == WXK_BACK )
403 {
404 const long pos = m_filterCtrl->GetLastPosition();
405 m_filterCtrl->Remove( pos - 1, pos );
406 }
407 else
408 {
409 bool isPrintable;
410 int ch = aEvent.GetUnicodeKey();
411
412 if( ch != WXK_NONE )
413 isPrintable = true;
414 else
415 {
416 ch = aEvent.GetKeyCode();
417 isPrintable = ch > WXK_SPACE && ch < WXK_START;
418 }
419
420 if( isPrintable )
421 {
422 wxString text( static_cast<wxChar>( ch ) );
423
424 // wxCHAR_HOOK chars have been converted to uppercase.
425 if( !aEvent.ShiftDown() )
426 text.MakeLower();
427
428 m_filterCtrl->AppendText( text );
429 }
430 }
431}
432
433
434void FILTER_COMBOPOPUP::doSetFocus( wxWindow* aWindow )
435{
436#ifndef __WXGTK__ // Cannot focus in non-toplevel windows on GTK
438#endif
439}
440
441
442FILTER_COMBOBOX::FILTER_COMBOBOX( wxWindow *parent, wxWindowID id, const wxPoint &pos,
443 const wxSize &size, long style ) :
444 wxComboCtrl( parent, id, wxEmptyString, pos, size, style|wxTE_PROCESS_ENTER ),
445 m_filterPopup( nullptr )
446{
447 UseAltPopupWindow();
448 Connect( wxEVT_CHAR_HOOK, wxKeyEventHandler( FILTER_COMBOBOX::onKeyDown ), nullptr, this );
449
450#ifdef __WXMSW__
451 // On Windows the listbox background doesn't have the right colour in dark mode
453 SetBackgroundColour( wxSystemSettings::GetColour( wxSYS_COLOUR_WINDOW ) );
454 else
455#endif
456 SetBackgroundColour( wxSystemSettings::GetColour( wxSYS_COLOUR_LISTBOX ) );
457}
458
459
460FILTER_COMBOBOX::FILTER_COMBOBOX( wxWindow* parent, wxWindowID id, const wxString& value,
461 const wxPoint& pos, const wxSize& size,
462 int count, wxString strings[], long style ) :
463 FILTER_COMBOBOX( parent, id, pos, size, style )
464{
465 // These arguments are just to match wxFormBuilder's expected API; they are not supported
466 wxASSERT( value.IsEmpty() && count == 0 && strings == nullptr );
467
470}
471
472
474{
475 Disconnect( wxEVT_CHAR_HOOK, wxKeyEventHandler( FILTER_COMBOBOX::onKeyDown ), nullptr, this );
476}
477
478
479void FILTER_COMBOBOX::SetStringList( const wxArrayString& aList )
480{
481 m_filterPopup->SetStringList( aList );
482}
483
484
485void FILTER_COMBOBOX::SetSelectedString( const wxString& aString )
486{
487 m_filterPopup->SetSelectedString( aString );
488}
489
490
492{
493 m_filterPopup = aPopup;
494 SetPopupControl( aPopup );
495}
496
497
498void FILTER_COMBOBOX::onKeyDown( wxKeyEvent& aEvt )
499{
500 int key = aEvt.GetKeyCode();
501
502 if( IsPopupShown() )
503 {
504 // If the popup is shown then it's CHAR_HOOK should be eating these before they
505 // even get to us. But just to be safe, we go ahead and skip.
506 aEvt.Skip();
507 }
508 // Shift-return accepts dialog
509 else if( ( key == WXK_RETURN || key == WXK_NUMPAD_ENTER ) && aEvt.ShiftDown() )
510 {
511 wxPostEvent( m_parent, wxCommandEvent( wxEVT_COMMAND_BUTTON_CLICKED, wxID_OK ) );
512 }
513 // Return, arrow-down and space-bar all open popup
514 else if( key == WXK_RETURN || key == WXK_NUMPAD_ENTER || key == WXK_DOWN
515 || key == WXK_NUMPAD_DOWN || key == WXK_SPACE )
516 {
517 Popup();
518 }
519 // Non-control characters go to filterbox in popup for read-only controls
520 else if( key > WXK_SPACE && key < WXK_START && ( GetWindowStyleFlag() & wxCB_READONLY ) )
521 {
522 Popup();
523 m_filterPopup->OnStartingKey( aEvt );
524 }
525 else
526 {
527 aEvt.Skip();
528 }
529}
virtual void SetSelectedString(const wxString &aString)
FILTER_COMBOPOPUP * m_filterPopup
void setFilterPopup(FILTER_COMBOPOPUP *aPopup)
virtual void SetStringList(const wxArrayString &aStringList)
FILTER_COMBOBOX(wxWindow *parent, wxWindowID id, const wxPoint &pos=wxDefaultPosition, const wxSize &size=wxDefaultSize, long style=0)
void onKeyDown(wxKeyEvent &aEvt)
void SetSelectedString(const wxString &aString)
void onEnter(wxCommandEvent &aEvent)
wxTextValidator * m_filterValidator
void onFilterEdit(wxCommandEvent &aEvent)
void SetStringValue(const wxString &aNetName) override
virtual void getListContent(wxArrayString &aStringList)
Fill the combobox list.
void onMouseClick(wxMouseEvent &aEvent)
void doStartingKey(wxKeyEvent &aEvent)
void rebuildList()
Call this to rebuild the list from the getListContent() method.
wxString getFilterValue() const
Get the current value of the filter control.
virtual void Accept()
void OnStartingKey(wxKeyEvent &aEvent)
std::optional< wxString > getSelectedValue() const
Get the currently selected value in the list, or std::nullopt.
void onIdle(wxIdleEvent &aEvent)
wxString GetStringValue() const override
wxArrayString m_stringList
wxSize GetAdjustedSize(int aMinWidth, int aPrefHeight, int aMaxHeight) override
wxListBox * m_listBox
wxEvtHandler * m_focusHandler
bool Create(wxWindow *aParent) override
void OnPopup() override
void doSetFocus(wxWindow *aWindow)
void SetStringList(const wxArrayString &aStringList)
void onKeyDown(wxKeyEvent &aEvent)
void onMouseMoved(const wxPoint aScreenPos)
wxTextCtrl * m_filterCtrl
#define _(s)
#define LIST_ITEM_PADDING
#define LIST_PADDING
wxDEFINE_EVENT(FILTERED_ITEM_SELECTED, wxCommandEvent)
wxPoint GetMousePosition()
Returns the mouse position in screen coordinates.
Definition wxgtk/ui.cpp:766
void ForceFocus(wxWindow *aWindow)
Pass the current focus to the window.
Definition wxgtk/ui.cpp:126
bool IsDarkTheme()
Determine if the desktop interface is currently using a dark theme or a light theme.
Definition wxgtk/ui.cpp:50
KICOMMON_API wxSize GetTextSize(const wxString &aSingleLine, wxWindow *aWindow)
Return the size of aSingleLine of text when it is rendered in aWindow using whatever font is currentl...
Definition ui_common.cpp:78
Functions to provide common constants and other functions to assist in making a consistent UI.