1 /*
2  * This program source code file is part of KiCad, a free EDA CAD application.
3  *
4  * Copyright (C) 2016 Chris Pavlina <pavlina.chris@gmail.com>
5  * Copyright (C) 2016-2021 KiCad Developers, see AUTHORS.txt for contributors.
6  *
7  * This program is free software; you can redistribute it and/or
8  * modify it under the terms of the GNU General Public License
9  * as published by the Free Software Foundation; either version 3
10  * of the License, or (at your option) any later version.
11  *
12  * This program is distributed in the hope that it will be useful,
13  * but WITHOUT ANY WARRANTY; without even the implied warranty of
14  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15  * GNU General Public License for more details.
16  *
17  * You should have received a copy of the GNU General Public License
18  * along with this program; if not, you may find one here:
19  * http://www.gnu.org/licenses/old-licenses/gpl-2.0.html
20  * or you may search the http://www.gnu.org website for the version 2 license,
21  * or you may write to the Free Software Foundation, Inc.,
22  * 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA
23  */
24 
25 #include <cctype>
26 
27 #include <confirm.h>
28 #include <widgets/widget_hotkey_list.h>
29 #include <tool/tool_event.h>
30 #include <dialog_shim.h>
31 
32 #include <wx/log.h>
33 #include <wx/dcclient.h>
34 #include <wx/menu.h>
35 #include <wx/msgdlg.h>
36 #include <wx/statline.h>
37 #include <wx/stattext.h>
38 #include <wx/treelist.h>
39 
40 /**
41  * Menu IDs for the hotkey context menu
42  */
43 enum ID_WHKL_MENU_IDS
44 {
45     ID_EDIT_HOTKEY = 2001,
46     ID_RESET,
47     ID_DEFAULT,
48     ID_CLEAR
49 };
50 
51 
52 /**
53  * Store the hotkey change data associated with each row.
54  *
55  * To change a hotkey, edit it via GetCurrentValue() in the row's client data, then call
56  * WIDGET_HOTKEY_LIST::UpdateFromClientData().
57  */
58 class WIDGET_HOTKEY_CLIENT_DATA : public wxClientData
59 {
60     HOTKEY&  m_changed_hotkey;
61 
62 public:
WIDGET_HOTKEY_CLIENT_DATA(HOTKEY & aChangedHotkey)63     WIDGET_HOTKEY_CLIENT_DATA( HOTKEY& aChangedHotkey ) :
64             m_changed_hotkey( aChangedHotkey )
65     {}
66 
GetChangedHotkey()67     HOTKEY& GetChangedHotkey() { return m_changed_hotkey; }
68 };
69 
70 
71 /**
72  * Dialog to prompt the user to enter a key.
73  */
74 class HK_PROMPT_DIALOG : public DIALOG_SHIM
75 {
76 public:
HK_PROMPT_DIALOG(wxWindow * aParent,wxWindowID aId,const wxString & aTitle,const wxString & aName,const wxString & aCurrentKey)77     HK_PROMPT_DIALOG( wxWindow* aParent, wxWindowID aId, const wxString& aTitle,
78                       const wxString& aName, const wxString& aCurrentKey ) :
79             DIALOG_SHIM( aParent, aId, aTitle, wxDefaultPosition, wxDefaultSize )
80     {
81         wxPanel* panel = new wxPanel( this, wxID_ANY, wxDefaultPosition, wxDefaultSize );
82         wxBoxSizer* sizer = new wxBoxSizer( wxVERTICAL );
83 
84         /* Dialog layout:
85          *
86          * inst_label........................
87          * ----------------------------------
88          *
89          * cmd_label_0      cmd_label_1         \
90          *                                      | fgsizer
91          * key_label_0      key_label_1         /
92          */
93 
94         wxStaticText* inst_label = new wxStaticText( panel, wxID_ANY, wxEmptyString,
95                                                      wxDefaultPosition, wxDefaultSize,
96                                                      wxALIGN_CENTRE_HORIZONTAL );
97 
98         inst_label->SetLabelText( _( "Press a new hotkey, or press Esc to cancel..." ) );
99         sizer->Add( inst_label, 0, wxALL, 5 );
100 
101         sizer->Add( new wxStaticLine( panel ), 0, wxALL | wxEXPAND, 2 );
102 
103         wxFlexGridSizer* fgsizer = new wxFlexGridSizer( 2 );
104 
105         wxStaticText* cmd_label_0 = new wxStaticText( panel, wxID_ANY, _( "Command:" ) );
106         fgsizer->Add( cmd_label_0, 0, wxALL | wxALIGN_CENTRE_VERTICAL, 5 );
107 
108         wxStaticText* cmd_label_1 = new wxStaticText( panel, wxID_ANY, wxEmptyString );
109         cmd_label_1->SetFont( cmd_label_1->GetFont().Bold() );
110         cmd_label_1->SetLabel( aName );
111         fgsizer->Add( cmd_label_1, 0, wxALL | wxALIGN_CENTRE_VERTICAL, 5 );
112 
113         wxStaticText* key_label_0 = new wxStaticText( panel, wxID_ANY, _( "Current key:" ) );
114         fgsizer->Add( key_label_0, 0, wxALL | wxALIGN_CENTRE_VERTICAL, 5 );
115 
116         wxStaticText* key_label_1 = new wxStaticText( panel, wxID_ANY, wxEmptyString );
117         key_label_1->SetFont( key_label_1->GetFont().Bold() );
118         key_label_1->SetLabel( aCurrentKey );
119         fgsizer->Add( key_label_1, 0, wxALL | wxALIGN_CENTRE_VERTICAL, 5 );
120 
121         sizer->Add( fgsizer, 1, wxEXPAND );
122 
123         // Wrap the sizer in a second to give a larger border around the whole dialog
124         wxBoxSizer* outer_sizer = new wxBoxSizer( wxVERTICAL );
125         outer_sizer->Add( sizer, 0, wxALL | wxEXPAND, 10 );
126         panel->SetSizer( outer_sizer );
127 
128         Layout();
129         outer_sizer->Fit( this );
130         Center();
131 
132         SetMinClientSize( GetClientSize() );
133 
134         // Binding both EVT_CHAR and EVT_CHAR_HOOK ensures that all key events, including
135         // specials like Tab and Return, are received, particularly on MSW.
136         panel->Bind( wxEVT_CHAR, &HK_PROMPT_DIALOG::OnChar, this );
137         panel->Bind( wxEVT_CHAR_HOOK, &HK_PROMPT_DIALOG::OnCharHook, this );
138         panel->Bind( wxEVT_KEY_UP, &HK_PROMPT_DIALOG::OnKeyUp, this );
139         SetInitialFocus( panel );
140     }
141 
PromptForKey(wxWindow * aParent,const wxString & aName,const wxString & aCurrentKey)142     static wxKeyEvent PromptForKey( wxWindow* aParent, const wxString& aName,
143                                     const wxString& aCurrentKey )
144     {
145         HK_PROMPT_DIALOG dialog( aParent, wxID_ANY, _( "Set Hotkey" ), aName, aCurrentKey );
146 
147         if( dialog.ShowModal() == wxID_OK )
148             return dialog.m_event;
149         else
150             return wxKeyEvent();
151     }
152 
153 protected:
OnCharHook(wxKeyEvent & aEvent)154     void OnCharHook( wxKeyEvent& aEvent ) override
155     {
156         // On certain platforms, EVT_CHAR_HOOK is the only handler that receives certain
157         // "special" keys. However, it doesn't always receive "normal" keys correctly. For
158         // example, with a US keyboard, it sees ? as shift+/.
159         //
160         // Untangling these incorrect keys would be too much trouble, so we bind both events,
161         // and simply skip the EVT_CHAR_HOOK if it receives a "normal" key.
162 
163         const enum wxKeyCode skipped_keys[] =
164         {
165             WXK_NONE, WXK_SHIFT, WXK_ALT, WXK_CONTROL, WXK_CAPITAL, WXK_NUMLOCK, WXK_SCROLL,
166             WXK_RAW_CONTROL
167         };
168 
169         int key = aEvent.GetKeyCode();
170 
171         for( wxKeyCode skipped_key : skipped_keys )
172         {
173             if( key == skipped_key )
174                 return;
175         }
176 
177         if( key <= 255 && isprint( key ) && !isspace( key ) )
178         {
179             // Let EVT_CHAR handle this one
180             aEvent.DoAllowNextEvent();
181 
182             // On Windows, wxEvent::Skip must NOT be called.
183             // On Linux and OSX, wxEvent::Skip MUST be called.
184             // No, I don't know why.
185 #ifndef __WXMSW__
186             aEvent.Skip();
187 #endif
188         }
189         else
190         {
191             OnChar( aEvent );
192         }
193     }
194 
OnChar(wxKeyEvent & aEvent)195     void OnChar( wxKeyEvent& aEvent )
196     {
197         m_event = aEvent;
198     }
199 
OnKeyUp(wxKeyEvent & aEvent)200     void OnKeyUp( wxKeyEvent& aEvent )
201     {
202         // If dialog opened using Enter key, prevent closing when releasing Enter.
203         if( m_event.GetEventType() != wxEVT_NULL )
204         {
205             /// This needs to occur in KeyUp, so that we don't pass the event back to pcbnew
206             wxPostEvent( this, wxCommandEvent( wxEVT_COMMAND_BUTTON_CLICKED, wxID_OK ) );
207         }
208     }
209 
210 private:
211     wxKeyEvent m_event;
212 };
213 
214 
215 /**
216  * Manage logic for filtering hotkeys based on user input.
217  */
218 class HOTKEY_FILTER
219 {
220 public:
HOTKEY_FILTER(const wxString & aFilterStr)221     HOTKEY_FILTER( const wxString& aFilterStr )
222     {
223         m_normalised_filter_str = aFilterStr.Upper();
224         m_valid = m_normalised_filter_str.size() > 0;
225     }
226 
227     /**
228      * Check if the filter matches the given hotkey
229      *
230      * @return true on match (or if filter is disabled)
231      */
FilterMatches(const HOTKEY & aHotkey) const232     bool FilterMatches( const HOTKEY& aHotkey ) const
233     {
234         if( !m_valid )
235             return true;
236 
237         // Match in the (translated) filter string
238         const auto normedInfo = wxGetTranslation( aHotkey.m_Actions[ 0 ]->GetLabel() ).Upper();
239 
240         if( normedInfo.Contains( m_normalised_filter_str ) )
241             return true;
242 
243         const wxString keyName = KeyNameFromKeyCode( aHotkey.m_EditKeycode );
244 
245         if( keyName.Upper().Contains( m_normalised_filter_str ) )
246             return true;
247 
248         return false;
249     }
250 
251 private:
252     bool     m_valid;
253     wxString m_normalised_filter_str;
254 };
255 
256 
getHKClientData(wxTreeListItem aItem)257 WIDGET_HOTKEY_CLIENT_DATA* WIDGET_HOTKEY_LIST::getHKClientData( wxTreeListItem aItem )
258 {
259     if( aItem.IsOk() )
260     {
261         wxClientData* data = GetItemData( aItem );
262 
263         if( data )
264             return static_cast<WIDGET_HOTKEY_CLIENT_DATA*>( data );
265     }
266 
267     return nullptr;
268 }
269 
270 
getExpectedHkClientData(wxTreeListItem aItem)271 WIDGET_HOTKEY_CLIENT_DATA* WIDGET_HOTKEY_LIST::getExpectedHkClientData( wxTreeListItem aItem )
272 {
273     const auto hkdata = getHKClientData( aItem );
274 
275     // This probably means a hotkey-only action is being attempted on
276     // a row that is not a hotkey (like a section heading)
277     wxASSERT_MSG( hkdata != nullptr, "No hotkey data found for list item" );
278 
279     return hkdata;
280 }
281 
282 
updateFromClientData()283 void WIDGET_HOTKEY_LIST::updateFromClientData()
284 {
285     for( wxTreeListItem i = GetFirstItem(); i.IsOk(); i = GetNextItem( i ) )
286     {
287         WIDGET_HOTKEY_CLIENT_DATA* hkdata = getHKClientData( i );
288 
289         if( hkdata )
290         {
291             const HOTKEY& changed_hk = hkdata->GetChangedHotkey();
292             wxString      label = changed_hk.m_Actions[ 0 ]->GetLabel();
293             wxString      key_text = KeyNameFromKeyCode( changed_hk.m_EditKeycode );
294             wxString      description = changed_hk.m_Actions[ 0 ]->GetDescription( false );
295 
296             if( label.IsEmpty() )
297                 label = changed_hk.m_Actions[ 0 ]->GetName();
298 
299             // mark unsaved changes
300             if( changed_hk.m_EditKeycode != changed_hk.m_Actions[ 0 ]->GetHotKey() )
301                 label += " *";
302 
303             SetItemText( i, 0, label );
304             SetItemText( i, 1, key_text);
305             SetItemText( i, 2, description );
306         }
307     }
308 }
309 
310 
changeHotkey(HOTKEY & aHotkey,long aKey)311 void WIDGET_HOTKEY_LIST::changeHotkey( HOTKEY& aHotkey, long aKey )
312 {
313     // See if this key code is handled in hotkeys names list
314     bool exists;
315     KeyNameFromKeyCode( aKey, &exists );
316 
317     if( exists && aHotkey.m_EditKeycode != aKey )
318     {
319         if( aKey == 0 || resolveKeyConflicts( aHotkey.m_Actions[ 0 ], aKey ) )
320             aHotkey.m_EditKeycode = aKey;
321     }
322 }
323 
324 
editItem(wxTreeListItem aItem)325 void WIDGET_HOTKEY_LIST::editItem( wxTreeListItem aItem )
326 {
327     WIDGET_HOTKEY_CLIENT_DATA* hkdata = getExpectedHkClientData( aItem );
328 
329     if( !hkdata )
330         return;
331 
332     wxString    name = GetItemText( aItem, 0 );
333     wxString    current_key = GetItemText( aItem, 1 );
334 
335     wxKeyEvent key_event = HK_PROMPT_DIALOG::PromptForKey( this, name, current_key );
336     long key = MapKeypressToKeycode( key_event );
337 
338     if( key )
339     {
340         auto it = m_reservedHotkeys.find( key );
341 
342         if( it != m_reservedHotkeys.end() )
343         {
344             wxString msg = wxString::Format(
345                     _( "'%s' is a reserved hotkey in KiCad and cannot be assigned." ),
346                     it->second );
347 
348             DisplayErrorMessage( this, msg );
349             return;
350         }
351 
352         changeHotkey( hkdata->GetChangedHotkey(), key );
353         updateFromClientData();
354     }
355 }
356 
357 
resetItem(wxTreeListItem aItem,int aResetId)358 void WIDGET_HOTKEY_LIST::resetItem( wxTreeListItem aItem, int aResetId )
359 {
360     WIDGET_HOTKEY_CLIENT_DATA* hkdata = getExpectedHkClientData( aItem );
361 
362     if( !hkdata )
363         return;
364 
365     HOTKEY& changed_hk = hkdata->GetChangedHotkey();
366 
367     if( aResetId == ID_RESET )
368         changeHotkey( changed_hk, changed_hk.m_Actions[ 0 ]->GetHotKey() );
369     else if( aResetId == ID_CLEAR )
370         changeHotkey( changed_hk, 0 );
371     else if( aResetId == ID_DEFAULT )
372         changeHotkey( changed_hk, changed_hk.m_Actions[ 0 ]->GetDefaultHotKey() );
373 
374     updateFromClientData();
375 }
376 
377 
onActivated(wxTreeListEvent & aEvent)378 void WIDGET_HOTKEY_LIST::onActivated( wxTreeListEvent& aEvent )
379 {
380     editItem( aEvent.GetItem());
381 }
382 
383 
onContextMenu(wxTreeListEvent & aEvent)384 void WIDGET_HOTKEY_LIST::onContextMenu( wxTreeListEvent& aEvent )
385 {
386     // Save the active event for use in OnMenu
387     m_context_menu_item = aEvent.GetItem();
388 
389     wxMenu menu;
390 
391     WIDGET_HOTKEY_CLIENT_DATA* hkdata = getHKClientData( m_context_menu_item );
392 
393     // Some actions only apply if the row is hotkey data
394     if( hkdata )
395     {
396         menu.Append( ID_EDIT_HOTKEY, _( "Edit..." ) );
397         menu.Append( ID_RESET, _( "Undo Changes" ) );
398         menu.Append( ID_CLEAR, _( "Clear Assigned Hotkey" ) );
399         menu.Append( ID_DEFAULT, _( "Restore Default" ) );
400         menu.Append( wxID_SEPARATOR );
401 
402         PopupMenu( &menu );
403     }
404 }
405 
406 
onMenu(wxCommandEvent & aEvent)407 void WIDGET_HOTKEY_LIST::onMenu( wxCommandEvent& aEvent )
408 {
409     switch( aEvent.GetId() )
410     {
411     case ID_EDIT_HOTKEY:editItem( m_context_menu_item );
412         break;
413 
414     case ID_RESET:
415     case ID_CLEAR:
416     case ID_DEFAULT:resetItem( m_context_menu_item, aEvent.GetId());
417         break;
418 
419     default:
420         wxFAIL_MSG( wxT( "Unknown ID in context menu event" ) );
421     }
422 }
423 
424 
resolveKeyConflicts(TOOL_ACTION * aAction,long aKey)425 bool WIDGET_HOTKEY_LIST::resolveKeyConflicts( TOOL_ACTION* aAction, long aKey )
426 {
427     HOTKEY* conflictingHotKey = nullptr;
428 
429     m_hk_store.CheckKeyConflicts( aAction, aKey, &conflictingHotKey );
430 
431     if( !conflictingHotKey )
432         return true;
433 
434     TOOL_ACTION* conflictingAction = conflictingHotKey->m_Actions[ 0 ];
435     wxString msg = wxString::Format( _( "'%s' is already assigned to '%s' in section '%s'. "
436                                         "Are you sure you want to change its assignment?" ),
437                                      KeyNameFromKeyCode( aKey ),
438                                      conflictingAction->GetLabel(),
439                                      HOTKEY_STORE::GetSectionName( conflictingAction ) );
440 
441     wxMessageDialog dlg( GetParent(), msg, _( "Confirm change" ), wxYES_NO | wxNO_DEFAULT );
442 
443     if( dlg.ShowModal() == wxID_YES )
444     {
445         // Reset the other hotkey
446         conflictingHotKey->m_EditKeycode = 0;
447         updateFromClientData();
448         return true;
449     }
450 
451     return false;
452 }
453 
454 
WIDGET_HOTKEY_LIST(wxWindow * aParent,HOTKEY_STORE & aHotkeyStore,bool aReadOnly)455 WIDGET_HOTKEY_LIST::WIDGET_HOTKEY_LIST( wxWindow* aParent, HOTKEY_STORE& aHotkeyStore,
456                                         bool aReadOnly ) :
457         wxTreeListCtrl( aParent, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxTL_SINGLE ),
458         m_hk_store( aHotkeyStore ),
459         m_readOnly( aReadOnly )
460 {
461     wxString command_header = _( "Command" );
462 
463     if( !m_readOnly )
464         command_header << " " << _( "(double-click to edit)" );
465 
466     AppendColumn( command_header, 450, wxALIGN_LEFT, wxCOL_RESIZABLE | wxCOL_SORTABLE );
467     AppendColumn( _( "Hotkey" ), 120, wxALIGN_LEFT, wxCOL_RESIZABLE | wxCOL_SORTABLE );
468     AppendColumn( _( "Description" ), 900, wxALIGN_LEFT, wxCOL_RESIZABLE | wxCOL_SORTABLE );
469 
470 
471 #if defined( __WXGTK__ )// && !wxCHECK_VERSION( 3, 1, 0 )
472     // Automatic column widths are broken in wxGTK 3.0.x; set min widths to ensure visibility
473     // They are also broken in wxGTK 3.1.4
474 
475     wxDataViewCtrl* dv = GetDataView();
476 
477     wxString longKey = wxT( "Ctrl+Alt+Shift+X" );
478     int      pad     = 20;
479 
480     dv->GetColumn( 0 )->SetMinWidth( dv->GetMainWindow()->GetTextExtent( command_header ).x + pad );
481     dv->GetColumn( 1 )->SetMinWidth( dv->GetMainWindow()->GetTextExtent( longKey ).x + pad );
482 
483     CallAfter( [&]()
484                {
485                    GetDataView()->Update();
486                } );
487     #endif
488 
489     std::vector<wxString> reserved_keys =
490     {
491             "Ctrl+Tab",
492             "Ctrl+Shift+Tab"
493     };
494 
495     for( auto& key : reserved_keys )
496     {
497         long code = KeyCodeFromKeyName( key );
498 
499         if( code )
500             m_reservedHotkeys[code] = key;
501         else
502         {
503             wxLogWarning( "Unknown reserved keycode %s\n", key );
504         }
505     }
506 
507     GetDataView()->SetIndent( 10 );
508 
509     if( !m_readOnly )
510     {
511         // The event only apply if the widget is in editable mode
512         Bind( wxEVT_TREELIST_ITEM_ACTIVATED, &WIDGET_HOTKEY_LIST::onActivated, this );
513         Bind( wxEVT_TREELIST_ITEM_CONTEXT_MENU, &WIDGET_HOTKEY_LIST::onContextMenu, this );
514         Bind( wxEVT_MENU, &WIDGET_HOTKEY_LIST::onMenu, this );
515     }
516 }
517 
518 
ApplyFilterString(const wxString & aFilterStr)519 void WIDGET_HOTKEY_LIST::ApplyFilterString( const wxString& aFilterStr )
520 {
521     updateShownItems( aFilterStr );
522 }
523 
524 
ResetAllHotkeys(bool aResetToDefault)525 void WIDGET_HOTKEY_LIST::ResetAllHotkeys( bool aResetToDefault )
526 {
527     Freeze();
528 
529     // Reset all the hotkeys, not just the ones shown
530     // Should not need to check conflicts, as the state we're about
531     // to set to a should be consistent
532     if( aResetToDefault )
533         m_hk_store.ResetAllHotkeysToDefault();
534     else
535         m_hk_store.ResetAllHotkeysToOriginal();
536 
537     updateFromClientData();
538     updateColumnWidths();
539 
540     Thaw();
541 }
542 
543 
TransferDataToControl()544 bool WIDGET_HOTKEY_LIST::TransferDataToControl()
545 {
546     updateShownItems( "" );
547     updateColumnWidths();
548 
549     return true;
550 }
551 
552 
updateColumnWidths()553 void WIDGET_HOTKEY_LIST::updateColumnWidths()
554 {
555     wxDataViewColumn* col = GetDataView()->GetColumn( 0 );
556     col->SetWidth( wxCOL_WIDTH_AUTOSIZE );
557     col->SetWidth( col->GetWidth() );
558 
559 #if defined( __WXGTK__ ) && !wxCHECK_VERSION( 3, 1, 0 )
560     col->SetResizeable( true );
561 #endif
562 
563     col = GetDataView()->GetColumn( 1 );
564     col->SetWidth( wxCOL_WIDTH_AUTOSIZE );
565     col->SetWidth( col->GetWidth() );
566 
567 #if defined( __WXGTK__ ) && !wxCHECK_VERSION( 3, 1, 0 )
568     col->SetResizeable( true );
569 #endif
570 }
571 
572 
updateShownItems(const wxString & aFilterStr)573 void WIDGET_HOTKEY_LIST::updateShownItems( const wxString& aFilterStr )
574 {
575     Freeze();
576     DeleteAllItems();
577 
578     HOTKEY_FILTER filter( aFilterStr );
579 
580     for( HOTKEY_SECTION& section: m_hk_store.GetSections() )
581     {
582         // Create parent tree item
583         wxTreeListItem parent = AppendItem( GetRootItem(), section.m_SectionName );
584 
585         for( HOTKEY& hotkey: section.m_HotKeys )
586         {
587             if( filter.FilterMatches( hotkey ) )
588             {
589                 wxTreeListItem item = AppendItem( parent, wxEmptyString );
590                 SetItemData( item, new WIDGET_HOTKEY_CLIENT_DATA( hotkey ) );
591             }
592         }
593 
594         Expand( parent );
595     }
596 
597     updateFromClientData();
598     Thaw();
599 }
600 
601 
TransferDataFromControl()602 bool WIDGET_HOTKEY_LIST::TransferDataFromControl()
603 {
604     m_hk_store.SaveAllHotkeys();
605     return true;
606 }
607 
608 
MapKeypressToKeycode(const wxKeyEvent & aEvent)609 long WIDGET_HOTKEY_LIST::MapKeypressToKeycode( const wxKeyEvent& aEvent )
610 {
611     long key = aEvent.GetKeyCode();
612     bool is_tab = aEvent.IsKeyInCategory( WXK_CATEGORY_TAB );
613 
614     if( key == WXK_ESCAPE )
615     {
616         return 0;
617     }
618     else
619     {
620         if( key >= 'a' && key <= 'z' )    // convert to uppercase
621             key = key + ('A' - 'a');
622 
623         // Remap Ctrl A (=1+GR_KB_CTRL) to Ctrl Z(=26+GR_KB_CTRL)
624         // to GR_KB_CTRL+'A' .. GR_KB_CTRL+'Z'
625         if( !is_tab && aEvent.ControlDown() && key >= WXK_CONTROL_A && key <= WXK_CONTROL_Z )
626             key += 'A' - 1;
627 
628         /* Disallow shift for keys that have two keycodes on them (e.g. number and
629          * punctuation keys) leaving only the "letter keys" of A-Z, tab and space
630          * Then, you can have, e.g. Ctrl-5 and Ctrl-% (GB layout)
631          * and Ctrl-( and Ctrl-5 (FR layout).
632          * Otherwise, you'd have to have to say Ctrl-Shift-5 on a FR layout
633          */
634         bool keyIsLetter = key >= 'A' && key <= 'Z';
635 
636         if( aEvent.ShiftDown() && ( keyIsLetter || key > 256 || key == 9 || key == 32 ) )
637             key |= MD_SHIFT;
638 
639         if( aEvent.ControlDown() )
640             key |= MD_CTRL;
641 
642         if( aEvent.AltDown() )
643             key |= MD_ALT;
644 
645         return key;
646     }
647 }
648