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