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