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 <tool/tool_action.h>
29 #include <eda_draw_frame.h>
30 #include <dialog_shim.h>
31 
32 
36 static const int HOTKEY_MIN_WIDTH = 100;
37 
38 
43 {
48 };
49 
50 
57 class WIDGET_HOTKEY_CLIENT_DATA : public wxClientData
58 {
60 
61 public:
62  WIDGET_HOTKEY_CLIENT_DATA( HOTKEY& aChangedHotkey )
63  : m_changed_hotkey( aChangedHotkey )
64  {}
65 
67 };
68 
69 
75 {
76  wxKeyEvent m_event;
77 
78 public:
79  HK_PROMPT_DIALOG( wxWindow* aParent, wxWindowID aId, const wxString& aTitle,
80  const wxString& aName, const wxString& aCurrentKey )
81  : DIALOG_SHIM( aParent, aId, aTitle, wxDefaultPosition, wxDefaultSize )
82  {
83  wxPanel* panel = new wxPanel( this, wxID_ANY, wxDefaultPosition, wxDefaultSize );
84  wxBoxSizer* sizer = new wxBoxSizer( wxVERTICAL );
85 
86  /* Dialog layout:
87  *
88  * inst_label........................
89  * ----------------------------------
90  *
91  * cmd_label_0 cmd_label_1 \
92  * | fgsizer
93  * key_label_0 key_label_1 /
94  */
95 
96  wxStaticText* inst_label = new wxStaticText( panel, wxID_ANY, wxEmptyString,
97  wxDefaultPosition, wxDefaultSize, wxALIGN_CENTRE_HORIZONTAL );
98 
99  inst_label->SetLabelText( _( "Press a new hotkey, or press Esc to cancel..." ) );
100  sizer->Add( inst_label, 0, wxALL, 5 );
101 
102  sizer->Add( new wxStaticLine( panel ), 0, wxALL | wxEXPAND, 2 );
103 
104  wxFlexGridSizer* fgsizer = new wxFlexGridSizer( 2 );
105 
106  wxStaticText* cmd_label_0 = new wxStaticText( panel, wxID_ANY, _( "Command:" ) );
107  fgsizer->Add( cmd_label_0, 0, wxALL | wxALIGN_CENTRE_VERTICAL, 5 );
108 
109  wxStaticText* cmd_label_1 = new wxStaticText( panel, wxID_ANY, wxEmptyString );
110  cmd_label_1->SetFont( cmd_label_1->GetFont().Bold() );
111  cmd_label_1->SetLabel( aName );
112  fgsizer->Add( cmd_label_1, 0, wxALL | wxALIGN_CENTRE_VERTICAL, 5 );
113 
114  wxStaticText* key_label_0 = new wxStaticText( panel, wxID_ANY, _( "Current key:" ) );
115  fgsizer->Add( key_label_0, 0, wxALL | wxALIGN_CENTRE_VERTICAL, 5 );
116 
117  wxStaticText* key_label_1 = new wxStaticText( panel, wxID_ANY, wxEmptyString );
118  key_label_1->SetFont( key_label_1->GetFont().Bold() );
119  key_label_1->SetLabel( aCurrentKey );
120  fgsizer->Add( key_label_1, 0, wxALL | wxALIGN_CENTRE_VERTICAL, 5 );
121 
122  sizer->Add( fgsizer, 1, wxEXPAND );
123 
124  // Wrap the sizer in a second to give a larger border around the whole dialog
125  wxBoxSizer* outer_sizer = new wxBoxSizer( wxVERTICAL );
126  outer_sizer->Add( sizer, 0, wxALL | wxEXPAND, 10 );
127  panel->SetSizer( outer_sizer );
128 
129  Layout();
130  outer_sizer->Fit( this );
131  Center();
132 
133  SetMinClientSize( GetClientSize() );
134 
135  // Binding both EVT_CHAR and EVT_CHAR_HOOK ensures that all key events,
136  // including specials like Tab and Return, are received, particularly
137  // on MSW.
138  panel->Bind( wxEVT_CHAR, &HK_PROMPT_DIALOG::OnChar, this );
139  panel->Bind( wxEVT_CHAR_HOOK, &HK_PROMPT_DIALOG::OnCharHook, this );
140  }
141 
142 
143  void OnCharHook( wxKeyEvent& aEvent )
144  {
145  // On certain platforms, EVT_CHAR_HOOK is the only handler that receives
146  // certain "special" keys. However, it doesn't always receive "normal"
147  // keys correctly. For example, with a US keyboard, it sees ? as shift+/.
148  //
149  // Untangling these incorrect keys would be too much trouble, so we bind
150  // both events, and simply skip the EVT_CHAR_HOOK if it receives a
151  // "normal" key.
152 
153  const enum wxKeyCode skipped_keys[] =
154  {
155  WXK_NONE, WXK_SHIFT, WXK_ALT, WXK_CONTROL, WXK_CAPITAL,
156  WXK_NUMLOCK, WXK_SCROLL, WXK_RAW_CONTROL
157  };
158 
159  int key = aEvent.GetKeyCode();
160 
161  for( wxKeyCode skipped_key : skipped_keys )
162  {
163  if( key == skipped_key )
164  return;
165  }
166 
167  if( key <= 255 && isprint( key ) && !isspace( key ) )
168  {
169  // Let EVT_CHAR handle this one
170  aEvent.DoAllowNextEvent();
171 
172  // On Windows, wxEvent::Skip must NOT be called.
173  // On Linux and OSX, wxEvent::Skip MUST be called.
174  // No, I don't know why.
175 #ifndef __WXMSW__
176  aEvent.Skip();
177 #endif
178  }
179  else
180  {
181  OnChar( aEvent );
182  }
183  }
184 
185 
186  void OnChar( wxKeyEvent& aEvent )
187  {
188  m_event = aEvent;
189  EndFlexible( wxID_OK );
190  }
191 
192 
196  void EndFlexible( int aRtnCode )
197  {
198  if( IsQuasiModal() )
199  EndQuasiModal( aRtnCode );
200  else
201  EndModal( aRtnCode );
202  }
203 
204 
205  static wxKeyEvent PromptForKey( wxWindow* aParent, const wxString& aName,
206  const wxString& aCurrentKey )
207  {
208  HK_PROMPT_DIALOG dialog( aParent, wxID_ANY, _( "Set Hotkey" ), aName, aCurrentKey );
209 
210  if( dialog.ShowModal() == wxID_OK )
211  return dialog.m_event;
212  else
213  return wxKeyEvent();
214  }
215 };
216 
217 
224 {
225 public:
226  HOTKEY_FILTER( const wxString& aFilterStr )
227  {
228  m_normalised_filter_str = aFilterStr.Upper();
229  m_valid = m_normalised_filter_str.size() > 0;
230  }
231 
239  bool FilterMatches( const HOTKEY& aHotkey ) const
240  {
241  if( !m_valid )
242  return true;
243 
244  // Match in the (translated) filter string
245  const auto normedInfo = wxGetTranslation( aHotkey.m_Actions[ 0 ]->GetLabel() ).Upper();
246  if( normedInfo.Contains( m_normalised_filter_str ) )
247  return true;
248 
249  const wxString keyName = KeyNameFromKeyCode( aHotkey.m_EditKeycode );
250  if( keyName.Upper().Contains( m_normalised_filter_str ) )
251  return true;
252 
253  return false;
254  }
255 
256 private:
257 
258  bool m_valid;
260 };
261 
262 
264 {
265  if( aItem.IsOk() )
266  {
267  wxClientData* data = GetItemData( aItem );
268 
269  if( data )
270  return static_cast<WIDGET_HOTKEY_CLIENT_DATA*>( data );
271  }
272 
273  return nullptr;
274 }
275 
276 
278 {
279  const auto hkdata = GetHKClientData( aItem );
280 
281  // This probably means a hotkey-only action is being attempted on
282  // a row that is not a hotkey (like a section heading)
283  wxASSERT_MSG( hkdata != nullptr, "No hotkey data found for list item" );
284 
285  return hkdata;
286 }
287 
288 
290 {
291  for( wxTreeListItem i = GetFirstItem(); i.IsOk(); i = GetNextItem( i ) )
292  {
294 
295  if( hkdata )
296  {
297  const auto& changed_hk = hkdata->GetChangedHotkey();
298  wxString label = changed_hk.m_Actions[ 0 ]->GetLabel();
299  wxString key_text = KeyNameFromKeyCode( changed_hk.m_EditKeycode );
300 
301  if( label.IsEmpty() )
302  label = changed_hk.m_Actions[ 0 ]->GetName();
303 
304  // mark unsaved changes
305  if( changed_hk.m_EditKeycode != changed_hk.m_Actions[ 0 ]->GetHotKey() )
306  key_text += " *";
307 
308  SetItemText( i, 0, label );
309  SetItemText( i, 1, key_text);
310  }
311  }
312 
313  // Trigger a resize in case column widths have changed
314  wxSizeEvent dummy_evt;
315  TWO_COLUMN_TREE_LIST::OnSize( dummy_evt );
316 }
317 
318 
319 void WIDGET_HOTKEY_LIST::changeHotkey( HOTKEY& aHotkey, long aKey )
320 {
321  // See if this key code is handled in hotkeys names list
322  bool exists;
323  KeyNameFromKeyCode( aKey, &exists );
324 
325  if( exists && aHotkey.m_EditKeycode != aKey )
326  {
327  if( aKey == 0 || ResolveKeyConflicts( aHotkey.m_Actions[ 0 ], aKey ) )
328  aHotkey.m_EditKeycode = aKey;
329  }
330 }
331 
332 
333 void WIDGET_HOTKEY_LIST::EditItem( wxTreeListItem aItem )
334 {
336 
337  if( !hkdata )
338  return;
339 
340  wxString name = GetItemText( aItem, 0 );
341  wxString current_key = GetItemText( aItem, 1 );
342 
343  wxKeyEvent key_event = HK_PROMPT_DIALOG::PromptForKey( GetParent(), name, current_key );
344  long key = MapKeypressToKeycode( key_event );
345 
346  if( key )
347  {
348  changeHotkey( hkdata->GetChangedHotkey(), key );
350  }
351 }
352 
353 
354 void WIDGET_HOTKEY_LIST::ResetItem( wxTreeListItem aItem, int aResetId )
355 {
357 
358  if( !hkdata )
359  return;
360 
361  auto& changed_hk = hkdata->GetChangedHotkey();
362 
363  if( aResetId == ID_RESET )
364  changeHotkey( changed_hk, changed_hk.m_Actions[ 0 ]->GetHotKey() );
365  else if( aResetId == ID_CLEAR )
366  changeHotkey( changed_hk, 0 );
367  else if( aResetId == ID_DEFAULT )
368  changeHotkey( changed_hk, changed_hk.m_Actions[ 0 ]->GetDefaultHotKey() );
369 
371 }
372 
373 
374 void WIDGET_HOTKEY_LIST::OnActivated( wxTreeListEvent& aEvent )
375 {
376  EditItem( aEvent.GetItem() );
377 }
378 
379 
380 void WIDGET_HOTKEY_LIST::OnContextMenu( wxTreeListEvent& aEvent )
381 {
382  // Save the active event for use in OnMenu
383  m_context_menu_item = aEvent.GetItem();
384 
385  wxMenu menu;
386 
388 
389  // Some actions only apply if the row is hotkey data
390  if( hkdata )
391  {
392  menu.Append( ID_EDIT_HOTKEY, _( "Edit..." ) );
393  menu.Append( ID_RESET, _( "Undo Changes" ) );
394  menu.Append( ID_CLEAR, _( "Clear Assigned Hotkey" ) );
395  menu.Append( ID_DEFAULT, _( "Restore Default" ) );
396  menu.Append( wxID_SEPARATOR );
397 
398  PopupMenu( &menu );
399  }
400 }
401 
402 
403 void WIDGET_HOTKEY_LIST::OnMenu( wxCommandEvent& aEvent )
404 {
405  switch( aEvent.GetId() )
406  {
407  case ID_EDIT_HOTKEY:
409  break;
410 
411  case ID_RESET:
412  case ID_CLEAR:
413  case ID_DEFAULT:
414  ResetItem( m_context_menu_item, aEvent.GetId() );
415  break;
416 
417  default:
418  wxFAIL_MSG( wxT( "Unknown ID in context menu event" ) );
419  }
420 }
421 
422 
424 {
425  HOTKEY* conflictingHotKey = nullptr;
426 
427  m_hk_store.CheckKeyConflicts( aAction, aKey, &conflictingHotKey );
428 
429  if( !conflictingHotKey )
430  return true;
431 
432  TOOL_ACTION* conflictingAction = conflictingHotKey->m_Actions[ 0 ];
433  wxString msg = wxString::Format( _( "\"%s\" is already assigned to \"%s\" in section \"%s\". "
434  "Are you sure you want to change its assignment?" ),
435  KeyNameFromKeyCode( aKey ),
436  conflictingAction->GetLabel(),
437  HOTKEY_STORE::GetSectionName( conflictingAction ) );
438 
439  wxMessageDialog dlg( GetParent(), msg, _( "Confirm change" ), wxYES_NO | wxNO_DEFAULT );
440 
441  if( dlg.ShowModal() == wxID_YES )
442  {
443  // Reset the other hotkey
444  conflictingHotKey->m_EditKeycode = 0;
446  return true;
447  }
448 
449  return false;
450 }
451 
452 
453 WIDGET_HOTKEY_LIST::WIDGET_HOTKEY_LIST( wxWindow* aParent, HOTKEY_STORE& aHotkeyStore,
454  bool aReadOnly )
455  : TWO_COLUMN_TREE_LIST( aParent, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxTL_SINGLE ),
456  m_hk_store( aHotkeyStore ),
457  m_readOnly( aReadOnly )
458 {
459  wxString command_header = _( "Command" );
460 
461  if( !m_readOnly )
462  command_header << " " << _( "(double-click to edit)" );
463 
464  AppendColumn( command_header );
465  AppendColumn( _( "Hotkey" ) );
466  SetRubberBandColumn( 0 );
468 
469  if( !m_readOnly )
470  {
471  // The event only apply if the widget is in editable mode
472  Bind( wxEVT_TREELIST_ITEM_ACTIVATED, &WIDGET_HOTKEY_LIST::OnActivated, this );
473  Bind( wxEVT_TREELIST_ITEM_CONTEXT_MENU, &WIDGET_HOTKEY_LIST::OnContextMenu, this );
474  Bind( wxEVT_MENU, &WIDGET_HOTKEY_LIST::OnMenu, this );
475  }
476 }
477 
478 
479 void WIDGET_HOTKEY_LIST::ApplyFilterString( const wxString& aFilterStr )
480 {
481  updateShownItems( aFilterStr );
482 }
483 
484 
485 void WIDGET_HOTKEY_LIST::ResetAllHotkeys( bool aResetToDefault )
486 {
487  Freeze();
488 
489  // Reset all the hotkeys, not just the ones shown
490  // Should not need to check conflicts, as the state we're about
491  // to set to a should be consistent
492  if( aResetToDefault )
494  else
496 
498  Thaw();
499 }
500 
501 
503 {
504  updateShownItems( "" );
505  return true;
506 }
507 
508 
509 void WIDGET_HOTKEY_LIST::updateShownItems( const wxString& aFilterStr )
510 {
511  Freeze();
512  DeleteAllItems();
513 
514  HOTKEY_FILTER filter( aFilterStr );
515 
516  for( HOTKEY_SECTION& section: m_hk_store.GetSections() )
517  {
518  // Create parent tree item
519  wxTreeListItem parent = AppendItem( GetRootItem(), section.m_SectionName );
520 
521  for( HOTKEY& hotkey: section.m_HotKeys )
522  {
523  if( filter.FilterMatches( hotkey ) )
524  {
525  wxTreeListItem item = AppendItem( parent, wxEmptyString );
526  SetItemData( item, new WIDGET_HOTKEY_CLIENT_DATA( hotkey ) );
527  }
528  }
529 
530  Expand( parent );
531  }
532 
534  Thaw();
535 }
536 
537 
539 {
541  return true;
542 }
543 
544 
545 long WIDGET_HOTKEY_LIST::MapKeypressToKeycode( const wxKeyEvent& aEvent )
546 {
547  long key = aEvent.GetKeyCode();
548 
549  if( key == WXK_ESCAPE )
550  {
551  return 0;
552  }
553  else
554  {
555  if( key >= 'a' && key <= 'z' ) // convert to uppercase
556  key = key + ('A' - 'a');
557 
558  // Remap Ctrl A (=1+GR_KB_CTRL) to Ctrl Z(=26+GR_KB_CTRL)
559  // to GR_KB_CTRL+'A' .. GR_KB_CTRL+'Z'
560  if( aEvent.ControlDown() && key >= WXK_CONTROL_A && key <= WXK_CONTROL_Z )
561  key += 'A' - 1;
562 
563  /* Disallow shift for keys that have two keycodes on them (e.g. number and
564  * punctuation keys) leaving only the "letter keys" of A-Z.
565  * Then, you can have, e.g. Ctrl-5 and Ctrl-% (GB layout)
566  * and Ctrl-( and Ctrl-5 (FR layout).
567  * Otherwise, you'd have to have to say Ctrl-Shift-5 on a FR layout
568  */
569  bool keyIsLetter = key >= 'A' && key <= 'Z';
570 
571  if( aEvent.ShiftDown() && ( keyIsLetter || key > 256 ) )
572  key |= MD_SHIFT;
573 
574  if( aEvent.ControlDown() )
575  key |= MD_CTRL;
576 
577  if( aEvent.AltDown() )
578  key |= MD_ALT;
579 
580  return key;
581  }
582 }
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.
Modified wxTreeListCtrl designed for use with two columns, with better column resizing.
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.
Class DIALOG_SHIM may sit in the inheritance tree between wxDialog and any class written by wxFormBui...
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.
static const int HOTKEY_MIN_WIDTH
Minimum width of the hotkey column.
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.
void SetClampedMinWidth(int aClampedMinWidth)
Set the minimum width of the non-rubber-band column.
wxTreeListItem m_context_menu_item
Class 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:127
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.
#define _(s)
void SetRubberBandColumn(int aRubberBandColumn)
Set the column number that will "rubber-band" (expand with available space).
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:61
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:205
void OnChar(wxKeyEvent &aEvent)
Class TOOL_ACTION.
Definition: tool_action.h:46
size_t i
Definition: json11.cpp:649
void ResetAllHotkeysToOriginal()
Resets every hotkey to the original values.
Class 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...
void OnSize(wxSizeEvent &aEvent)
Override buggy wxTreeListCtrl size handler.
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)
Class 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.