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 // Only process when the popup is actually visible to avoid ClientToScreen warnings.
257 // Use IsShownOnScreen() instead of IsShown() because wx may report the window as shown
258 // before GTK has fully realized it, causing ClientToScreen to fail.
259 if( !IsShownOnScreen() )
260 return;
261
262 // Generate synthetic (but reliable) MouseMoved events
263 static wxPoint lastPos;
264 wxPoint screenPos = KIPLATFORM::UI::GetMousePosition();
265
266 if( screenPos != lastPos )
267 {
268 lastPos = screenPos;
269 onMouseMoved( screenPos );
270 }
271
272 if( m_focusHandler )
273 {
274 m_filterCtrl->PushEventHandler( m_focusHandler );
275 m_focusHandler = nullptr;
276 }
277}
278
279
280// Hot-track the mouse (for focus and listbox selection)
281void FILTER_COMBOPOPUP::onMouseMoved( const wxPoint aScreenPos )
282{
283 if( m_listBox->GetScreenRect().Contains( aScreenPos ) )
284 {
286
287 wxPoint relativePos = m_listBox->ScreenToClient( aScreenPos );
288 int item = m_listBox->HitTest( relativePos );
289
290 if( item >= 0 )
291 m_listBox->SetSelection( item );
292 }
293 else if( m_filterCtrl->GetScreenRect().Contains( aScreenPos ) )
294 {
296 }
297}
298
299
300void FILTER_COMBOPOPUP::onMouseClick( wxMouseEvent& aEvent )
301{
302 // Accept a click event from anywhere. Different platform implementations have
303 // different foibles with regard to transient popups and their children.
304 if( aEvent.GetEventObject() == m_listBox )
305 {
306 m_listBox->SetSelection( m_listBox->HitTest( aEvent.GetPosition() ) );
307 Accept();
308 return;
309 }
310
311 wxWindow* window = dynamic_cast<wxWindow*>( aEvent.GetEventObject() );
312
313 if( window )
314 {
315 wxPoint screenPos = window->ClientToScreen( aEvent.GetPosition() );
316
317 if( m_listBox->GetScreenRect().Contains( screenPos ) )
318 {
319 wxPoint localPos = m_listBox->ScreenToClient( screenPos );
320
321 m_listBox->SetSelection( m_listBox->HitTest( localPos ) );
322 Accept();
323 }
324 }
325}
326
327
328void FILTER_COMBOPOPUP::onKeyDown( wxKeyEvent& aEvent )
329{
330 switch( aEvent.GetKeyCode() )
331 {
332 // Control keys go to the parent combobox
333 case WXK_TAB:
334 Dismiss();
335
336 m_parent->NavigateIn( ( aEvent.ShiftDown() ? 0 : wxNavigationKeyEvent::IsForward ) |
337 ( aEvent.ControlDown() ? wxNavigationKeyEvent::WinChange : 0 ) );
338 break;
339
340 case WXK_ESCAPE:
341 Dismiss();
342 break;
343
344 case WXK_RETURN:
345 case WXK_NUMPAD_ENTER:
346 Accept();
347 break;
348
349 // Arrows go to the list box
350 case WXK_DOWN:
351 case WXK_NUMPAD_DOWN:
353 m_listBox->SetSelection( std::min( m_listBox->GetSelection() + 1,
354 (int) m_listBox->GetCount() - 1 ) );
355 break;
356
357 case WXK_UP:
358 case WXK_NUMPAD_UP:
360 m_listBox->SetSelection( std::max( m_listBox->GetSelection() - 1, 0 ) );
361 break;
362
363 // Everything else goes to the filter textbox
364 default:
365 if( !m_filterCtrl->HasFocus() )
366 {
368
369 // Because we didn't have focus we missed our chance to have the native widget
370 // handle the keystroke. We'll have to do the first character ourselves.
371 doStartingKey( aEvent );
372 }
373 else
374 {
375 // On some platforms a wxComboFocusHandler will have been pushed which
376 // unhelpfully gives the event right back to the popup. Make sure the filter
377 // control is going to get the event.
378 if( m_filterCtrl->GetEventHandler() != m_filterCtrl )
379 m_focusHandler = m_filterCtrl->PopEventHandler();
380
381 aEvent.Skip();
382 }
383 break;
384 }
385}
386
387
388void FILTER_COMBOPOPUP::onEnter( wxCommandEvent& aEvent )
389{
390 Accept();
391}
392
393
394void FILTER_COMBOPOPUP::onFilterEdit( wxCommandEvent& aEvent )
395{
396 rebuildList();
397 updateSize();
398
399 if( m_listBox->GetCount() > 0 )
400 m_listBox->SetSelection( 0 );
401}
402
403
404void FILTER_COMBOPOPUP::doStartingKey( wxKeyEvent& aEvent )
405{
406 if( aEvent.GetKeyCode() == WXK_BACK )
407 {
408 const long pos = m_filterCtrl->GetLastPosition();
409 m_filterCtrl->Remove( pos - 1, pos );
410 }
411 else
412 {
413 bool isPrintable;
414 int ch = aEvent.GetUnicodeKey();
415
416 if( ch != WXK_NONE )
417 isPrintable = true;
418 else
419 {
420 ch = aEvent.GetKeyCode();
421 isPrintable = ch > WXK_SPACE && ch < WXK_START;
422 }
423
424 if( isPrintable )
425 {
426 wxString text( static_cast<wxChar>( ch ) );
427
428 // wxCHAR_HOOK chars have been converted to uppercase.
429 if( !aEvent.ShiftDown() )
430 text.MakeLower();
431
432 m_filterCtrl->AppendText( text );
433 }
434 }
435}
436
437
438void FILTER_COMBOPOPUP::doSetFocus( wxWindow* aWindow )
439{
440#ifndef __WXGTK__ // Cannot focus in non-toplevel windows on GTK
442#endif
443}
444
445
446FILTER_COMBOBOX::FILTER_COMBOBOX( wxWindow *parent, wxWindowID id, const wxPoint &pos,
447 const wxSize &size, long style ) :
448 wxComboCtrl( parent, id, wxEmptyString, pos, size, style|wxTE_PROCESS_ENTER ),
449 m_filterPopup( nullptr )
450{
451 UseAltPopupWindow();
452 Connect( wxEVT_CHAR_HOOK, wxKeyEventHandler( FILTER_COMBOBOX::onKeyDown ), nullptr, this );
453
454#ifdef __WXMSW__
455 // On Windows the listbox background doesn't have the right colour in dark mode
457 SetBackgroundColour( wxSystemSettings::GetColour( wxSYS_COLOUR_WINDOW ) );
458 else
459#endif
460 SetBackgroundColour( wxSystemSettings::GetColour( wxSYS_COLOUR_LISTBOX ) );
461}
462
463
464FILTER_COMBOBOX::FILTER_COMBOBOX( wxWindow* parent, wxWindowID id, const wxString& value,
465 const wxPoint& pos, const wxSize& size,
466 int count, wxString strings[], long style ) :
467 FILTER_COMBOBOX( parent, id, pos, size, style )
468{
469 // These arguments are just to match wxFormBuilder's expected API; they are not supported
470 wxASSERT( value.IsEmpty() && count == 0 && strings == nullptr );
471
474}
475
476
478{
479 Disconnect( wxEVT_CHAR_HOOK, wxKeyEventHandler( FILTER_COMBOBOX::onKeyDown ), nullptr, this );
480}
481
482
483void FILTER_COMBOBOX::SetStringList( const wxArrayString& aList )
484{
485 m_filterPopup->SetStringList( aList );
486}
487
488
489void FILTER_COMBOBOX::SetSelectedString( const wxString& aString )
490{
491 m_filterPopup->SetSelectedString( aString );
492}
493
494
496{
497 m_filterPopup = aPopup;
498 SetPopupControl( aPopup );
499}
500
501
502void FILTER_COMBOBOX::onKeyDown( wxKeyEvent& aEvt )
503{
504 int key = aEvt.GetKeyCode();
505
506 if( IsPopupShown() )
507 {
508 // If the popup is shown then it's CHAR_HOOK should be eating these before they
509 // even get to us. But just to be safe, we go ahead and skip.
510 aEvt.Skip();
511 }
512 // Shift-return accepts dialog
513 else if( ( key == WXK_RETURN || key == WXK_NUMPAD_ENTER ) && aEvt.ShiftDown() )
514 {
515 wxPostEvent( m_parent, wxCommandEvent( wxEVT_COMMAND_BUTTON_CLICKED, wxID_OK ) );
516 }
517 // Return, arrow-down and space-bar all open popup
518 else if( key == WXK_RETURN || key == WXK_NUMPAD_ENTER || key == WXK_DOWN
519 || key == WXK_NUMPAD_DOWN || key == WXK_SPACE )
520 {
521 Popup();
522 }
523 // Non-control characters go to filterbox in popup for read-only controls
524 else if( key > WXK_SPACE && key < WXK_START && ( GetWindowStyleFlag() & wxCB_READONLY ) )
525 {
526 Popup();
527 m_filterPopup->OnStartingKey( aEvt );
528 }
529 else
530 {
531 aEvt.Skip();
532 }
533}
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
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:706
void ForceFocus(wxWindow *aWindow)
Pass the current focus to the window.
Definition wxgtk/ui.cpp:125
bool IsDarkTheme()
Determine if the desktop interface is currently using a dark theme or a light theme.
Definition wxgtk/ui.cpp:49
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.