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 (C) 2024 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 5
42 #define LIST_PADDING 5
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, 0, 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,
92 this );
93 m_listBox->Connect( wxEVT_LEFT_DOWN, wxMouseEventHandler( FILTER_COMBOPOPUP::onMouseClick ),
94 nullptr, this );
95 m_filterCtrl->Connect( wxEVT_TEXT, wxCommandEventHandler( FILTER_COMBOPOPUP::onFilterEdit ),
96 nullptr, this );
97 m_filterCtrl->Connect( wxEVT_TEXT_ENTER, wxCommandEventHandler( FILTER_COMBOPOPUP::onEnter ),
98 nullptr, this );
99
100 // <enter> in a ListBox comes in as a double-click on GTK
101 m_listBox->Connect( wxEVT_COMMAND_LISTBOX_DOUBLECLICKED,
102 wxCommandEventHandler( FILTER_COMBOPOPUP::onEnter ), nullptr, this );
103
104 return true;
105}
106
107
108void FILTER_COMBOPOPUP::SetStringValue( const wxString& aNetName )
109{
110 // shouldn't be here (combo is read-only)
111}
112
113
115{
116 // While it can sometimes be useful to keep the filter, it's always unexpected.
117 // Better to clear it.
118 m_filterCtrl->Clear();
119
120 m_listBox->SetStringSelection( GetStringValue() );
121 m_filterCtrl->SetFocus();
122
123 // The updateSize() call in GetAdjustedSize() leaves the height off-by-one for
124 // some reason, so do it again.
125 updateSize();
126}
127
128
129void FILTER_COMBOPOPUP:: OnStartingKey( wxKeyEvent& aEvent )
130{
132 doStartingKey( aEvent );
133}
134
135
136wxSize FILTER_COMBOPOPUP::GetAdjustedSize( int aMinWidth, int aPrefHeight, int aMaxHeight )
137{
138 // Called when the popup is first shown. Stash the minWidth and maxHeight so we
139 // can use them later when refreshing the sizes after filter changes.
140 m_minPopupWidth = aMinWidth;
141 m_maxPopupHeight = aMaxHeight;
142
143 return updateSize();
144}
145
146
148{
149 wxArrayString newList;
150 getListContent( newList );
151
152 m_listBox->Set( newList );
153}
154
155
156std::optional<wxString> FILTER_COMBOPOPUP::getSelectedValue() const
157{
158 int selection = m_listBox->GetSelection();
159
160 if( selection >= 0 )
161 return m_listBox->GetString( selection );
162
163 return std::nullopt;
164}
165
166
168{
169 return m_filterCtrl->GetValue().Trim( true ).Trim( false );
170}
171
172
174{
175 int listTop = m_listBox->GetRect().y;
176 int itemHeight = KIUI::GetTextSize( wxT( "Xy" ), this ).y + LIST_ITEM_PADDING;
177 int listHeight = m_listBox->GetCount() * itemHeight + LIST_PADDING;
178
179 if( listTop + listHeight >= m_maxPopupHeight )
180 listHeight = m_maxPopupHeight - listTop - 1;
181
182 int listWidth = m_minPopupWidth;
183
184 for( size_t i = 0; i < m_listBox->GetCount(); ++i )
185 {
186 int itemWidth = KIUI::GetTextSize( m_listBox->GetString( i ), m_listBox ).x;
187 listWidth = std::max( listWidth, itemWidth + LIST_PADDING * 3 );
188 }
189
190 wxSize listSize( listWidth, listHeight );
191 wxSize popupSize( listWidth, listTop + listHeight );
192
193 SetSize( popupSize ); // us
194 GetParent()->SetSize( popupSize ); // the window that wxComboCtrl put us in
195
196 m_listBox->SetMinSize( listSize );
197 m_listBox->SetSize( listSize );
198
199 return popupSize;
200}
201
202
203void FILTER_COMBOPOPUP::onIdle( wxIdleEvent& aEvent )
204{
205 // Generate synthetic (but reliable) MouseMoved events
206 static wxPoint lastPos;
207 wxPoint screenPos = KIPLATFORM::UI::GetMousePosition();
208
209 if( screenPos != lastPos )
210 {
211 lastPos = screenPos;
212 onMouseMoved( screenPos );
213 }
214
215 if( m_focusHandler )
216 {
217 m_filterCtrl->PushEventHandler( m_focusHandler );
218 m_focusHandler = nullptr;
219 }
220}
221
222
223// Hot-track the mouse (for focus and listbox selection)
224void FILTER_COMBOPOPUP::onMouseMoved( const wxPoint aScreenPos )
225{
226 if( m_listBox->GetScreenRect().Contains( aScreenPos ) )
227 {
229
230 wxPoint relativePos = m_listBox->ScreenToClient( aScreenPos );
231 int item = m_listBox->HitTest( relativePos );
232
233 if( item >= 0 )
234 m_listBox->SetSelection( item );
235 }
236 else if( m_filterCtrl->GetScreenRect().Contains( aScreenPos ) )
237 {
239 }
240}
241
242
243void FILTER_COMBOPOPUP::onMouseClick( wxMouseEvent& aEvent )
244{
245 // Accept a click event from anywhere. Different platform implementations have
246 // different foibles with regard to transient popups and their children.
247
248 if( aEvent.GetEventObject() == m_listBox )
249 {
250 m_listBox->SetSelection( m_listBox->HitTest( aEvent.GetPosition() ) );
251 Accept();
252 return;
253 }
254
255 wxWindow* window = dynamic_cast<wxWindow*>( aEvent.GetEventObject() );
256
257 if( window )
258 {
259 wxPoint screenPos = window->ClientToScreen( aEvent.GetPosition() );
260
261 if( m_listBox->GetScreenRect().Contains( screenPos ) )
262 {
263 wxPoint localPos = m_listBox->ScreenToClient( screenPos );
264
265 m_listBox->SetSelection( m_listBox->HitTest( localPos ) );
266 Accept();
267 }
268 }
269}
270
271
272void FILTER_COMBOPOPUP::onKeyDown( wxKeyEvent& aEvent )
273{
274 switch( aEvent.GetKeyCode() )
275 {
276 // Control keys go to the parent combobox
277 case WXK_TAB:
278 Dismiss();
279
280 m_parent->NavigateIn( ( aEvent.ShiftDown() ? 0 : wxNavigationKeyEvent::IsForward ) |
281 ( aEvent.ControlDown() ? wxNavigationKeyEvent::WinChange : 0 ) );
282 break;
283
284 case WXK_ESCAPE:
285 Dismiss();
286 break;
287
288 case WXK_RETURN:
289 case WXK_NUMPAD_ENTER:
290 Accept();
291 break;
292
293 // Arrows go to the list box
294 case WXK_DOWN:
295 case WXK_NUMPAD_DOWN:
297 m_listBox->SetSelection( std::min( m_listBox->GetSelection() + 1, (int) m_listBox->GetCount() - 1 ) );
298 break;
299
300 case WXK_UP:
301 case WXK_NUMPAD_UP:
303 m_listBox->SetSelection( std::max( m_listBox->GetSelection() - 1, 0 ) );
304 break;
305
306 // Everything else goes to the filter textbox
307 default:
308 if( !m_filterCtrl->HasFocus() )
309 {
311
312 // Because we didn't have focus we missed our chance to have the native widget
313 // handle the keystroke. We'll have to do the first character ourselves.
314 doStartingKey( aEvent );
315 }
316 else
317 {
318 // On some platforms a wxComboFocusHandler will have been pushed which
319 // unhelpfully gives the event right back to the popup. Make sure the filter
320 // control is going to get the event.
321 if( m_filterCtrl->GetEventHandler() != m_filterCtrl )
322 m_focusHandler = m_filterCtrl->PopEventHandler();
323
324 aEvent.Skip();
325 }
326 break;
327 }
328}
329
330
331void FILTER_COMBOPOPUP::onEnter( wxCommandEvent& aEvent )
332{
333 Accept();
334}
335
336
337void FILTER_COMBOPOPUP::onFilterEdit( wxCommandEvent& aEvent )
338{
339 rebuildList();
340 updateSize();
341
342 if( m_listBox->GetCount() > 0 )
343 m_listBox->SetSelection( 0 );
344}
345
346
347void FILTER_COMBOPOPUP::doStartingKey( wxKeyEvent& aEvent )
348{
349 if( aEvent.GetKeyCode() == WXK_BACK )
350 {
351 const long pos = m_filterCtrl->GetLastPosition();
352 m_filterCtrl->Remove( pos - 1, pos );
353 }
354 else
355 {
356 bool isPrintable;
357 int ch = aEvent.GetUnicodeKey();
358
359 if( ch != WXK_NONE )
360 isPrintable = true;
361 else
362 {
363 ch = aEvent.GetKeyCode();
364 isPrintable = ch > WXK_SPACE && ch < WXK_START;
365 }
366
367 if( isPrintable )
368 {
369 wxString text( static_cast<wxChar>( ch ) );
370
371 // wxCHAR_HOOK chars have been converted to uppercase.
372 if( !aEvent.ShiftDown() )
373 text.MakeLower();
374
375 m_filterCtrl->AppendText( text );
376 }
377 }
378}
379
380
381void FILTER_COMBOPOPUP::doSetFocus( wxWindow* aWindow )
382{
383#ifndef __WXGTK__ // Cannot focus in non-toplevel windows on GTK
385#endif
386}
387
388
389FILTER_COMBOBOX::FILTER_COMBOBOX( wxWindow *parent, wxWindowID id, const wxPoint &pos,
390 const wxSize &size, long style ) :
391 wxComboCtrl( parent, id, wxEmptyString, pos, size, style|wxCB_READONLY|wxTE_PROCESS_ENTER )
392{
393 UseAltPopupWindow();
394 Connect( wxEVT_CHAR_HOOK, wxKeyEventHandler( FILTER_COMBOBOX::onKeyDown ), nullptr, this );
395
396 SetBackgroundColour( wxSystemSettings::GetColour( wxSYS_COLOUR_LISTBOX ) );
397}
398
399
401{
402 Disconnect( wxEVT_CHAR_HOOK, wxKeyEventHandler( FILTER_COMBOBOX::onKeyDown ), nullptr, this );
403}
404
405
406void FILTER_COMBOBOX::setFilterPopup( std::unique_ptr<FILTER_COMBOPOPUP> aPopup )
407{
408 m_filterPopup = aPopup.get();
409 SetPopupControl( aPopup.release() );
410}
411
412
413void FILTER_COMBOBOX::onKeyDown( wxKeyEvent& aEvt )
414{
415 int key = aEvt.GetKeyCode();
416
417 if( IsPopupShown() )
418 {
419 // If the popup is shown then it's CHAR_HOOK should be eating these before they
420 // even get to us. But just to be safe, we go ahead and skip.
421 aEvt.Skip();
422 }
423
424 // Shift-return accepts dialog
425 else if( ( key == WXK_RETURN || key == WXK_NUMPAD_ENTER ) && aEvt.ShiftDown() )
426 {
427 wxPostEvent( m_parent, wxCommandEvent( wxEVT_COMMAND_BUTTON_CLICKED, wxID_OK ) );
428 }
429
430 // Return, arrow-down and space-bar all open popup
431 else if( key == WXK_RETURN || key == WXK_NUMPAD_ENTER || key == WXK_DOWN
432 || key == WXK_NUMPAD_DOWN || key == WXK_SPACE )
433 {
434 Popup();
435 }
436
437 // Non-control characters go to filterbox in popup
438 else if( key > WXK_SPACE && key < WXK_START )
439 {
440 Popup();
442 }
443
444 else
445 {
446 aEvt.Skip();
447 }
448}
FILTER_COMBOPOPUP * m_filterPopup
FILTER_COMBOBOX(wxWindow *parent, wxWindowID id, const wxPoint &pos=wxDefaultPosition, const wxSize &size=wxDefaultSize, long style=0)
void onKeyDown(wxKeyEvent &aEvt)
void setFilterPopup(std::unique_ptr< FILTER_COMBOPOPUP > aPopup)
void onEnter(wxCommandEvent &aEvent)
wxTextValidator * m_filterValidator
void onFilterEdit(wxCommandEvent &aEvent)
void SetStringValue(const wxString &aNetName) override
void onMouseClick(wxMouseEvent &aEvent)
void doStartingKey(wxKeyEvent &aEvent)
virtual void getListContent(wxArrayString &aListToFill)=0
Implement this to fill in the given list.
void rebuildList()
Call this to rebuild the list from the getListContent() method.
wxString getFilterValue() const
Get the current value of the filter control.
void OnStartingKey(wxKeyEvent &aEvent)
std::optional< wxString > getSelectedValue() const
Get the currently selected value in the list, or std::nullopt.
void onIdle(wxIdleEvent &aEvent)
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)
virtual void Accept()=0
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:620
void ForceFocus(wxWindow *aWindow)
Pass the current focus to the window.
Definition: wxgtk/ui.cpp:67
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:77
Functions to provide common constants and other functions to assist in making a consistent UI.