KiCad PCB EDA Suite
widget_hotkey_list.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) 2016 Chris Pavlina <pavlina.chris@gmail.com>
5  * Copyright (C) 2016-2019 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>
27 #include <wx/statline.h>
28 #include <wx/treelist.h>
29 #include <tool/tool_action.h>
30 #include <dialog_shim.h>
31 
32 
37 {
42 };
43 
44 
51 class WIDGET_HOTKEY_CLIENT_DATA : public wxClientData
52 {
54 
55 public:
56  WIDGET_HOTKEY_CLIENT_DATA( HOTKEY& aChangedHotkey )
57  : m_changed_hotkey( aChangedHotkey )
58  {}
59 
61 };
62 
63 
69 {
70  wxKeyEvent m_event;
71 
72 public:
73  HK_PROMPT_DIALOG( wxWindow* aParent, wxWindowID aId, const wxString& aTitle,
74  const wxString& aName, const wxString& aCurrentKey )
75  : DIALOG_SHIM( aParent, aId, aTitle, wxDefaultPosition, wxDefaultSize )
76  {
77  wxPanel* panel = new wxPanel( this, wxID_ANY, wxDefaultPosition, wxDefaultSize );
78  wxBoxSizer* sizer = new wxBoxSizer( wxVERTICAL );
79 
80  /* Dialog layout:
81  *
82  * inst_label........................
83  * ----------------------------------
84  *
85  * cmd_label_0 cmd_label_1 \
86  * | fgsizer
87  * key_label_0 key_label_1 /
88  */
89 
90  wxStaticText* inst_label = new wxStaticText( panel, wxID_ANY, wxEmptyString,
91  wxDefaultPosition, wxDefaultSize, wxALIGN_CENTRE_HORIZONTAL );
92 
93  inst_label->SetLabelText( _( "Press a new hotkey, or press Esc to cancel..." ) );
94  sizer->Add( inst_label, 0, wxALL, 5 );
95 
96  sizer->Add( new wxStaticLine( panel ), 0, wxALL | wxEXPAND, 2 );
97 
98  wxFlexGridSizer* fgsizer = new wxFlexGridSizer( 2 );
99 
100  wxStaticText* cmd_label_0 = new wxStaticText( panel, wxID_ANY, _( "Command:" ) );
101  fgsizer->Add( cmd_label_0, 0, wxALL | wxALIGN_CENTRE_VERTICAL, 5 );
102 
103  wxStaticText* cmd_label_1 = new wxStaticText( panel, wxID_ANY, wxEmptyString );
104  cmd_label_1->SetFont( cmd_label_1->GetFont().Bold() );
105  cmd_label_1->SetLabel( aName );
106  fgsizer->Add( cmd_label_1, 0, wxALL | wxALIGN_CENTRE_VERTICAL, 5 );
107 
108  wxStaticText* key_label_0 = new wxStaticText( panel, wxID_ANY, _( "Current key:" ) );
109  fgsizer->Add( key_label_0, 0, wxALL | wxALIGN_CENTRE_VERTICAL, 5 );
110 
111  wxStaticText* key_label_1 = new wxStaticText( panel, wxID_ANY, wxEmptyString );
112  key_label_1->SetFont( key_label_1->GetFont().Bold() );
113  key_label_1->SetLabel( aCurrentKey );
114  fgsizer->Add( key_label_1, 0, wxALL | wxALIGN_CENTRE_VERTICAL, 5 );
115 
116  sizer->Add( fgsizer, 1, wxEXPAND );
117 
118  // Wrap the sizer in a second to give a larger border around the whole dialog
119  wxBoxSizer* outer_sizer = new wxBoxSizer( wxVERTICAL );
120  outer_sizer->Add( sizer, 0, wxALL | wxEXPAND, 10 );
121  panel->SetSizer( outer_sizer );
122 
123  Layout();
124  outer_sizer->Fit( this );
125  Center();
126 
127  SetMinClientSize( GetClientSize() );
128 
129  // Binding both EVT_CHAR and EVT_CHAR_HOOK ensures that all key events,
130  // including specials like Tab and Return, are received, particularly
131  // on MSW.
132  panel->Bind( wxEVT_CHAR, &HK_PROMPT_DIALOG::OnChar, this );
133  panel->Bind( wxEVT_CHAR_HOOK, &HK_PROMPT_DIALOG::OnCharHook, this );
134  }
135 
136 
137  void OnCharHook( wxKeyEvent& aEvent )
138  {
139  // On certain platforms, EVT_CHAR_HOOK is the only handler that receives
140  // certain "special" keys. However, it doesn't always receive "normal"
141  // keys correctly. For example, with a US keyboard, it sees ? as shift+/.
142  //
143  // Untangling these incorrect keys would be too much trouble, so we bind
144  // both events, and simply skip the EVT_CHAR_HOOK if it receives a
145  // "normal" key.
146 
147  const enum wxKeyCode skipped_keys[] =
148  {
149  WXK_NONE, WXK_SHIFT, WXK_ALT, WXK_CONTROL, WXK_CAPITAL,
150  WXK_NUMLOCK, WXK_SCROLL, WXK_RAW_CONTROL
151  };
152 
153  int key = aEvent.GetKeyCode();
154 
155  for( wxKeyCode skipped_key : skipped_keys )
156  {
157  if( key == skipped_key )
158  return;
159  }
160 
161  if( key <= 255 && isprint( key ) && !isspace( key ) )
162  {
163  // Let EVT_CHAR handle this one
164  aEvent.DoAllowNextEvent();
165 
166  // On Windows, wxEvent::Skip must NOT be called.
167  // On Linux and OSX, wxEvent::Skip MUST be called.
168  // No, I don't know why.
169 #ifndef __WXMSW__
170  aEvent.Skip();
171 #endif
172  }
173  else
174  {
175  OnChar( aEvent );
176  }
177  }
178 
179 
180  void OnChar( wxKeyEvent& aEvent )
181  {
182  m_event = aEvent;
183  EndFlexible( wxID_OK );
184  }
185 
186 
190  void EndFlexible( int aRtnCode )
191  {
192  if( IsQuasiModal() )
193  EndQuasiModal( aRtnCode );
194  else
195  EndModal( aRtnCode );
196  }
197 
198 
199  static wxKeyEvent PromptForKey( wxWindow* aParent, const wxString& aName,
200  const wxString& aCurrentKey )
201  {
202  HK_PROMPT_DIALOG dialog( aParent, wxID_ANY, _( "Set Hotkey" ), aName, aCurrentKey );
203 
204  if( dialog.ShowModal() == wxID_OK )
205  return dialog.m_event;
206  else
207  return wxKeyEvent();
208  }
209 };
210 
211 
218 {
219 public:
220  HOTKEY_FILTER( const wxString& aFilterStr )
221  {
222  m_normalised_filter_str = aFilterStr.Upper();
223  m_valid = m_normalised_filter_str.size() > 0;
224  }
225 
233  bool FilterMatches( const HOTKEY& aHotkey ) const
234  {
235  if( !m_valid )
236  return true;
237 
238  // Match in the (translated) filter string
239  const auto normedInfo = wxGetTranslation( aHotkey.m_Actions[ 0 ]->GetLabel() ).Upper();
240  if( normedInfo.Contains( m_normalised_filter_str ) )
241  return true;
242 
243  const wxString keyName = KeyNameFromKeyCode( aHotkey.m_EditKeycode );
244  if( keyName.Upper().Contains( m_normalised_filter_str ) )
245  return true;
246 
247  return false;
248  }
249 
250 private:
251 
252  bool m_valid;
254 };
255 
256 
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 
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 
284 {
285  for( wxTreeListItem i = GetFirstItem(); i.IsOk(); i = GetNextItem( i ) )
286  {
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 
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 
325 void WIDGET_HOTKEY_LIST::EditItem( wxTreeListItem aItem )
326 {
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( GetParent(), name, current_key );
336  long key = MapKeypressToKeycode( key_event );
337 
338  if( key )
339  {
340  changeHotkey( hkdata->GetChangedHotkey(), key );
342  }
343 }
344 
345 
346 void WIDGET_HOTKEY_LIST::ResetItem( wxTreeListItem aItem, int aResetId )
347 {
349 
350  if( !hkdata )
351  return;
352 
353  HOTKEY& changed_hk = hkdata->GetChangedHotkey();
354 
355  if( aResetId == ID_RESET )
356  changeHotkey( changed_hk, changed_hk.m_Actions[ 0 ]->GetHotKey() );
357  else if( aResetId == ID_CLEAR )
358  changeHotkey( changed_hk, 0 );
359  else if( aResetId == ID_DEFAULT )
360  changeHotkey( changed_hk, changed_hk.m_Actions[ 0 ]->GetDefaultHotKey() );
361 
363 }
364 
365 
366 void WIDGET_HOTKEY_LIST::OnActivated( wxTreeListEvent& aEvent )
367 {
368  EditItem( aEvent.GetItem() );
369 }
370 
371 
372 void WIDGET_HOTKEY_LIST::OnContextMenu( wxTreeListEvent& aEvent )
373 {
374  // Save the active event for use in OnMenu
375  m_context_menu_item = aEvent.GetItem();
376 
377  wxMenu menu;
378 
380 
381  // Some actions only apply if the row is hotkey data
382  if( hkdata )
383  {
384  menu.Append( ID_EDIT_HOTKEY, _( "Edit..." ) );
385  menu.Append( ID_RESET, _( "Undo Changes" ) );
386  menu.Append( ID_CLEAR, _( "Clear Assigned Hotkey" ) );
387  menu.Append( ID_DEFAULT, _( "Restore Default" ) );
388  menu.Append( wxID_SEPARATOR );
389 
390  PopupMenu( &menu );
391  }
392 }
393 
394 
395 void WIDGET_HOTKEY_LIST::OnMenu( wxCommandEvent& aEvent )
396 {
397  switch( aEvent.GetId() )
398  {
399  case ID_EDIT_HOTKEY:
401  break;
402 
403  case ID_RESET:
404  case ID_CLEAR:
405  case ID_DEFAULT:
406  ResetItem( m_context_menu_item, aEvent.GetId() );
407  break;
408 
409  default:
410  wxFAIL_MSG( wxT( "Unknown ID in context menu event" ) );
411  }
412 }
413 
414 
416 {
417  HOTKEY* conflictingHotKey = nullptr;
418 
419  m_hk_store.CheckKeyConflicts( aAction, aKey, &conflictingHotKey );
420 
421  if( !conflictingHotKey )
422  return true;
423 
424  TOOL_ACTION* conflictingAction = conflictingHotKey->m_Actions[ 0 ];
425  wxString msg = wxString::Format( _( "\"%s\" is already assigned to \"%s\" in section \"%s\". "
426  "Are you sure you want to change its assignment?" ),
427  KeyNameFromKeyCode( aKey ),
428  conflictingAction->GetLabel(),
429  HOTKEY_STORE::GetSectionName( conflictingAction ) );
430 
431  wxMessageDialog dlg( GetParent(), msg, _( "Confirm change" ), wxYES_NO | wxNO_DEFAULT );
432 
433  if( dlg.ShowModal() == wxID_YES )
434  {
435  // Reset the other hotkey
436  conflictingHotKey->m_EditKeycode = 0;
438  return true;
439  }
440 
441  return false;
442 }
443 
444 
445 WIDGET_HOTKEY_LIST::WIDGET_HOTKEY_LIST( wxWindow* aParent, HOTKEY_STORE& aHotkeyStore,
446  bool aReadOnly )
447  : wxTreeListCtrl( aParent, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxTL_SINGLE ),
448  m_hk_store( aHotkeyStore ),
449  m_readOnly( aReadOnly )
450 {
451  wxString command_header = _( "Command" );
452 
453  if( !m_readOnly )
454  command_header << " " << _( "(double-click to edit)" );
455 
456  AppendColumn( command_header, 320, wxALIGN_LEFT, wxCOL_RESIZABLE | wxCOL_SORTABLE );
457  AppendColumn( _( "Hotkey" ), 110, wxALIGN_LEFT, wxCOL_RESIZABLE | wxCOL_SORTABLE );
458  AppendColumn( _( "Description" ), 1000, wxALIGN_LEFT, wxCOL_RESIZABLE | wxCOL_SORTABLE );
459  GetDataView()->SetIndent( 10 );
460 
461  if( !m_readOnly )
462  {
463  // The event only apply if the widget is in editable mode
464  Bind( wxEVT_TREELIST_ITEM_ACTIVATED, &WIDGET_HOTKEY_LIST::OnActivated, this );
465  Bind( wxEVT_TREELIST_ITEM_CONTEXT_MENU, &WIDGET_HOTKEY_LIST::OnContextMenu, this );
466  Bind( wxEVT_MENU, &WIDGET_HOTKEY_LIST::OnMenu, this );
467  }
468 }
469 
470 
471 void WIDGET_HOTKEY_LIST::ApplyFilterString( const wxString& aFilterStr )
472 {
473  updateShownItems( aFilterStr );
474 }
475 
476 
477 void WIDGET_HOTKEY_LIST::ResetAllHotkeys( bool aResetToDefault )
478 {
479  Freeze();
480 
481  // Reset all the hotkeys, not just the ones shown
482  // Should not need to check conflicts, as the state we're about
483  // to set to a should be consistent
484  if( aResetToDefault )
486  else
488 
490  Thaw();
491 }
492 
493 
495 {
496  updateShownItems( "" );
497  return true;
498 }
499 
500 
501 void WIDGET_HOTKEY_LIST::updateShownItems( const wxString& aFilterStr )
502 {
503  Freeze();
504  DeleteAllItems();
505 
506  HOTKEY_FILTER filter( aFilterStr );
507 
508  for( HOTKEY_SECTION& section: m_hk_store.GetSections() )
509  {
510  // Create parent tree item
511  wxTreeListItem parent = AppendItem( GetRootItem(), section.m_SectionName );
512 
513  for( HOTKEY& hotkey: section.m_HotKeys )
514  {
515  if( filter.FilterMatches( hotkey ) )
516  {
517  wxTreeListItem item = AppendItem( parent, wxEmptyString );
518  SetItemData( item, new WIDGET_HOTKEY_CLIENT_DATA( hotkey ) );
519  }
520  }
521 
522  Expand( parent );
523  }
524 
526  Thaw();
527 }
528 
529 
531 {
533  return true;
534 }
535 
536 
537 long WIDGET_HOTKEY_LIST::MapKeypressToKeycode( const wxKeyEvent& aEvent )
538 {
539  long key = aEvent.GetKeyCode();
540 
541  if( key == WXK_ESCAPE )
542  {
543  return 0;
544  }
545  else
546  {
547  if( key >= 'a' && key <= 'z' ) // convert to uppercase
548  key = key + ('A' - 'a');
549 
550  // Remap Ctrl A (=1+GR_KB_CTRL) to Ctrl Z(=26+GR_KB_CTRL)
551  // to GR_KB_CTRL+'A' .. GR_KB_CTRL+'Z'
552  if( aEvent.ControlDown() && key >= WXK_CONTROL_A && key <= WXK_CONTROL_Z )
553  key += 'A' - 1;
554 
555  /* Disallow shift for keys that have two keycodes on them (e.g. number and
556  * punctuation keys) leaving only the "letter keys" of A-Z.
557  * Then, you can have, e.g. Ctrl-5 and Ctrl-% (GB layout)
558  * and Ctrl-( and Ctrl-5 (FR layout).
559  * Otherwise, you'd have to have to say Ctrl-Shift-5 on a FR layout
560  */
561  bool keyIsLetter = key >= 'A' && key <= 'Z';
562 
563  if( aEvent.ShiftDown() && ( keyIsLetter || key > 256 ) )
564  key |= MD_SHIFT;
565 
566  if( aEvent.ControlDown() )
567  key |= MD_CTRL;
568 
569  if( aEvent.AltDown() )
570  key |= MD_ALT;
571 
572  return key;
573  }
574 }
void OnActivated(wxTreeListEvent &aEvent)
Method OnActivated Handle activation of a row.
HK_PROMPT_DIALOG(wxWindow *aParent, wxWindowID aId, const wxString &aTitle, const wxString &aName, const wxString &aCurrentKey)
static long MapKeypressToKeycode(const wxKeyEvent &aEvent)
Static method MapKeypressToKeycode Map a keypress event to the correct key code for use as a hotkey.
void changeHotkey(HOTKEY &aHotkey, long aKey)
Attempt to change the given hotkey to the given key code.
A class that contains a set of hotkeys, arranged into "sections" and provides some book-keeping funct...
Definition: hotkey_store.h:61
void OnMenu(wxCommandEvent &aEvent)
Method OnMenu Handle activation of a context menu item.
bool TransferDataToControl()
Method TransferDataToControl Load the hotkey data from the store into the control.
void SaveAllHotkeys()
Persist all changes to hotkeys in the store to the underlying data structures.
Dialog helper object to sit in the inheritance tree between wxDialog and any class written by wxFormB...
Definition: dialog_shim.h:83
wxString m_normalised_filter_str
ID_WHKL_MENU_IDS
Menu IDs for the hotkey context menu.
static wxKeyEvent PromptForKey(wxWindow *aParent, const wxString &aName, const wxString &aCurrentKey)
void OnCharHook(wxKeyEvent &aEvent)
WIDGET_HOTKEY_CLIENT_DATA * getExpectedHkClientData(wxTreeListItem aItem)
Get the WIDGET_HOTKEY_CLIENT_DATA form an item and assert if it isn't found.
HOTKEY_STORE & m_hk_store
void UpdateFromClientData()
Method UpdateFromClientData Refresh the visible text on the widget from the rows' client data objects...
void updateShownItems(const wxString &aFilterStr)
Method updateShownItems.
wxTreeListItem m_context_menu_item
WIDGET_HOTKEY_CLIENT_DATA Stores the hotkey change data associated with each row.
void ResetAllHotkeysToDefault()
Reset every hotkey in the store to the default values.
bool IsQuasiModal()
Definition: dialog_shim.h:123
bool ResolveKeyConflicts(TOOL_ACTION *aAction, long aKey)
Method ResolveKeyConflicts Check if we can set a hotkey, and prompt the user if there is a conflict b...
void ApplyFilterString(const wxString &aFilterStr)
Method ApplyFilterString Apply a filter string to the hotkey list, selecting which hotkeys to show.
WIDGET_HOTKEY_LIST(wxWindow *aParent, HOTKEY_STORE &aHotkeyStore, bool aReadOnly)
Constructor WIDGET_HOTKEY_LIST Create a WIDGET_HOTKEY_LIST.
bool CheckKeyConflicts(TOOL_ACTION *aAction, long aKey, HOTKEY **aConflict)
Check whether the given key conflicts with anything in this store.
wxDataViewItem GetNextItem(wxDataViewCtrl const &aView, wxDataViewItem const &aItem)
Get the next item in list order.
std::vector< TOOL_ACTION * > m_Actions
Definition: hotkey_store.h:35
void ResetItem(wxTreeListItem aItem, int aResetId)
Method ResetItem Reset the item to either the default, the value when the dialog was opened,...
void EndQuasiModal(int retCode)
bool FilterMatches(const HOTKEY &aHotkey) const
Method FilterMatches.
wxString GetLabel() const
Definition: tool_action.cpp:69
std::vector< HOTKEY_SECTION > & GetSections()
Get the list of sections managed by this store.
void EndFlexible(int aRtnCode)
End the dialog whether modal or quasimodal.
const char * name
Definition: DXF_plotter.cpp:60
void Format(OUTPUTFORMATTER *out, int aNestLevel, int aCtl, CPTREE &aTree)
Function Format outputs a PTREE into s-expression format via an OUTPUTFORMATTER derivative.
Definition: ptree.cpp:201
void OnChar(wxKeyEvent &aEvent)
#define _(s)
Definition: 3d_actions.cpp:33
Represents a single user action.
Definition: tool_action.h:44
void ResetAllHotkeysToOriginal()
Resets every hotkey to the original values.
HOTKEY_FILTER.
WIDGET_HOTKEY_CLIENT_DATA * GetHKClientData(wxTreeListItem aItem)
Method GetHKClientData Return the WIDGET_HOTKEY_CLIENT_DATA for the given item, or NULL if the item i...
wxString KeyNameFromKeyCode(int aKeycode, bool *aIsFound)
Function KeyNameFromKeyCode return the key name from the key code Only some wxWidgets key values are ...
void EditItem(wxTreeListItem aItem)
Method EditItem Prompt the user for a new hotkey given a list item.
WIDGET_HOTKEY_CLIENT_DATA(HOTKEY &aChangedHotkey)
int m_EditKeycode
Definition: hotkey_store.h:36
HOTKEY_FILTER(const wxString &aFilterStr)
HK_PROMPT_DIALOG Dialog to prompt the user to enter a key.
void ResetAllHotkeys(bool aResetToDefault)
Set hotkeys in the control to default or original values.
void OnContextMenu(wxTreeListEvent &aEvent)
Method OnContextMenu Handle right-click on a row.
static wxString GetSectionName(TOOL_ACTION *aAction)
bool TransferDataFromControl()
Method TransferDataFromControl Save the hotkey data from the control.