You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

642 lines
17 KiB

10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
  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-2017 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. #include <cctype>
  25. #include <widgets/widget_hotkey_list.h>
  26. #include <wx/statline.h>
  27. #include <draw_frame.h>
  28. #include <dialog_shim.h>
  29. /**
  30. * Minimum width of the hotkey column
  31. */
  32. static const int HOTKEY_MIN_WIDTH = 100;
  33. /**
  34. * Menu IDs for the hotkey context menu
  35. */
  36. enum ID_WHKL_MENU_IDS
  37. {
  38. ID_EDIT = 2001,
  39. ID_RESET,
  40. ID_DEFAULT,
  41. ID_RESET_ALL,
  42. ID_DEFAULT_ALL,
  43. };
  44. /**
  45. * Class WIDGET_HOTKEY_CLIENT_DATA
  46. * Stores the hotkey change data associated with each row. To change a
  47. * hotkey, edit it via GetCurrentValue() in the row's client data, then call
  48. * WIDGET_HOTKEY_LIST::UpdateFromClientData().
  49. */
  50. class WIDGET_HOTKEY_CLIENT_DATA : public wxClientData
  51. {
  52. CHANGED_HOTKEY& m_changed_hotkey;
  53. public:
  54. WIDGET_HOTKEY_CLIENT_DATA( CHANGED_HOTKEY& aChangedHotkey )
  55. : m_changed_hotkey( aChangedHotkey )
  56. {}
  57. CHANGED_HOTKEY& GetChangedHotkey() { return m_changed_hotkey; }
  58. };
  59. /**
  60. * Class HK_PROMPT_DIALOG
  61. * Dialog to prompt the user to enter a key.
  62. */
  63. class HK_PROMPT_DIALOG : public DIALOG_SHIM
  64. {
  65. wxKeyEvent m_event;
  66. public:
  67. HK_PROMPT_DIALOG( wxWindow* aParent, wxWindowID aId, const wxString& aTitle,
  68. const wxString& aName, const wxString& aCurrentKey )
  69. : DIALOG_SHIM( aParent, aId, aTitle, wxDefaultPosition, wxDefaultSize )
  70. {
  71. wxPanel* panel = new wxPanel( this, wxID_ANY, wxDefaultPosition, wxDefaultSize );
  72. wxBoxSizer* sizer = new wxBoxSizer( wxVERTICAL );
  73. /* Dialog layout:
  74. *
  75. * inst_label........................
  76. * ----------------------------------
  77. *
  78. * cmd_label_0 cmd_label_1 \
  79. * | fgsizer
  80. * key_label_0 key_label_1 /
  81. */
  82. wxStaticText* inst_label = new wxStaticText( panel, wxID_ANY, wxEmptyString,
  83. wxDefaultPosition, wxDefaultSize, wxALIGN_CENTRE_HORIZONTAL );
  84. inst_label->SetLabelText( _( "Press a new hotkey, or press Esc to cancel..." ) );
  85. sizer->Add( inst_label, 0, wxALL, 5 );
  86. sizer->Add( new wxStaticLine( panel ), 0, wxALL | wxEXPAND, 2 );
  87. wxFlexGridSizer* fgsizer = new wxFlexGridSizer( 2 );
  88. wxStaticText* cmd_label_0 = new wxStaticText( panel, wxID_ANY, _( "Command:" ) );
  89. fgsizer->Add( cmd_label_0, 0, wxALL | wxALIGN_CENTRE_VERTICAL, 5 );
  90. wxStaticText* cmd_label_1 = new wxStaticText( panel, wxID_ANY, wxEmptyString );
  91. cmd_label_1->SetFont( cmd_label_1->GetFont().Bold() );
  92. cmd_label_1->SetLabel( aName );
  93. fgsizer->Add( cmd_label_1, 0, wxALL | wxALIGN_CENTRE_VERTICAL, 5 );
  94. wxStaticText* key_label_0 = new wxStaticText( panel, wxID_ANY, _( "Current key:" ) );
  95. fgsizer->Add( key_label_0, 0, wxALL | wxALIGN_CENTRE_VERTICAL, 5 );
  96. wxStaticText* key_label_1 = new wxStaticText( panel, wxID_ANY, wxEmptyString );
  97. key_label_1->SetFont( key_label_1->GetFont().Bold() );
  98. key_label_1->SetLabel( aCurrentKey );
  99. fgsizer->Add( key_label_1, 0, wxALL | wxALIGN_CENTRE_VERTICAL, 5 );
  100. sizer->Add( fgsizer, 1, wxEXPAND );
  101. // Wrap the sizer in a second to give a larger border around the whole dialog
  102. wxBoxSizer* outer_sizer = new wxBoxSizer( wxVERTICAL );
  103. outer_sizer->Add( sizer, 0, wxALL | wxEXPAND, 10 );
  104. panel->SetSizer( outer_sizer );
  105. Layout();
  106. outer_sizer->Fit( this );
  107. Center();
  108. SetMinClientSize( GetClientSize() );
  109. // Binding both EVT_CHAR and EVT_CHAR_HOOK ensures that all key events,
  110. // including specials like Tab and Return, are received, particularly
  111. // on MSW.
  112. panel->Bind( wxEVT_CHAR, &HK_PROMPT_DIALOG::OnChar, this );
  113. panel->Bind( wxEVT_CHAR_HOOK, &HK_PROMPT_DIALOG::OnCharHook, this );
  114. }
  115. void OnCharHook( wxKeyEvent& aEvent )
  116. {
  117. // On certain platforms, EVT_CHAR_HOOK is the only handler that receives
  118. // certain "special" keys. However, it doesn't always receive "normal"
  119. // keys correctly. For example, with a US keyboard, it sees ? as shift+/.
  120. //
  121. // Untangling these incorrect keys would be too much trouble, so we bind
  122. // both events, and simply skip the EVT_CHAR_HOOK if it receives a
  123. // "normal" key.
  124. const enum wxKeyCode skipped_keys[] =
  125. {
  126. WXK_NONE, WXK_SHIFT, WXK_ALT, WXK_CONTROL, WXK_CAPITAL,
  127. WXK_NUMLOCK, WXK_SCROLL, WXK_RAW_CONTROL
  128. };
  129. int key = aEvent.GetKeyCode();
  130. for( size_t i = 0; i < sizeof( skipped_keys ) / sizeof( skipped_keys[0] ); ++i )
  131. {
  132. if( key == skipped_keys[i] )
  133. return;
  134. }
  135. if( key <= 255 && isprint( key ) && !isspace( key ) )
  136. {
  137. // Let EVT_CHAR handle this one
  138. aEvent.DoAllowNextEvent();
  139. // On Windows, wxEvent::Skip must NOT be called.
  140. // On Linux and OSX, wxEvent::Skip MUST be called.
  141. // No, I don't know why.
  142. #ifndef __WXMSW__
  143. aEvent.Skip();
  144. #endif
  145. }
  146. else
  147. {
  148. OnChar( aEvent );
  149. }
  150. }
  151. void OnChar( wxKeyEvent& aEvent )
  152. {
  153. m_event = aEvent;
  154. EndFlexible( wxID_OK );
  155. }
  156. /**
  157. * End the dialog whether modal or quasimodal
  158. */
  159. void EndFlexible( int aRtnCode )
  160. {
  161. if( IsQuasiModal() )
  162. EndQuasiModal( aRtnCode );
  163. else
  164. EndModal( aRtnCode );
  165. }
  166. static wxKeyEvent PromptForKey( wxWindow* aParent, const wxString& aName,
  167. const wxString& aCurrentKey )
  168. {
  169. HK_PROMPT_DIALOG dialog( aParent, wxID_ANY, _( "Set Hotkey" ), aName, aCurrentKey );
  170. if( dialog.ShowModal() == wxID_OK )
  171. {
  172. return dialog.m_event;
  173. }
  174. else
  175. {
  176. wxKeyEvent dummy;
  177. return dummy;
  178. }
  179. }
  180. };
  181. /**
  182. * Class HOTKEY_FILTER
  183. *
  184. * Class to manage logic for filtering hotkeys based on user input
  185. */
  186. class HOTKEY_FILTER
  187. {
  188. public:
  189. HOTKEY_FILTER( const wxString& aFilterStr )
  190. {
  191. m_normalised_filter_str = aFilterStr.Upper();
  192. m_valid = m_normalised_filter_str.size() > 0;
  193. }
  194. /**
  195. * Method FilterMatches
  196. *
  197. * Checks if the filter matches the given hotkey
  198. *
  199. * @return true on match (or if filter is disabled)
  200. */
  201. bool FilterMatches( const EDA_HOTKEY& aHotkey ) const
  202. {
  203. if( !m_valid )
  204. return true;
  205. // Match in the (translated) filter string
  206. const auto normedInfo = wxGetTranslation( aHotkey.m_InfoMsg ).Upper();
  207. if( normedInfo.Contains( m_normalised_filter_str ) )
  208. return true;
  209. const wxString keyName = KeyNameFromKeyCode( aHotkey.m_KeyCode );
  210. if( keyName.Upper().Contains( m_normalised_filter_str ) )
  211. return true;
  212. return false;
  213. }
  214. private:
  215. bool m_valid;
  216. wxString m_normalised_filter_str;
  217. };
  218. WIDGET_HOTKEY_CLIENT_DATA* WIDGET_HOTKEY_LIST::GetHKClientData( wxTreeListItem aItem )
  219. {
  220. if( aItem.IsOk() )
  221. {
  222. wxClientData* data = GetItemData( aItem );
  223. if( !data )
  224. {
  225. return NULL;
  226. }
  227. else
  228. {
  229. return static_cast<WIDGET_HOTKEY_CLIENT_DATA*>( data );
  230. }
  231. }
  232. else
  233. {
  234. return NULL;
  235. }
  236. }
  237. WIDGET_HOTKEY_CLIENT_DATA* WIDGET_HOTKEY_LIST::GetSelHKClientData()
  238. {
  239. return GetHKClientData( GetSelection() );
  240. }
  241. WIDGET_HOTKEY_CLIENT_DATA* WIDGET_HOTKEY_LIST::getExpectedHkClientData( wxTreeListItem aItem )
  242. {
  243. const auto hkdata = GetHKClientData( aItem );
  244. // This probably means a hotkey-only action is being attempted on
  245. // a row that is not a hotkey (like a section heading)
  246. wxASSERT_MSG( hkdata != nullptr, "No hotkey data found for list item" );
  247. return hkdata;
  248. }
  249. void WIDGET_HOTKEY_LIST::UpdateFromClientData()
  250. {
  251. for( wxTreeListItem i = GetFirstItem(); i.IsOk(); i = GetNextItem( i ) )
  252. {
  253. WIDGET_HOTKEY_CLIENT_DATA* hkdata = GetHKClientData( i );
  254. if( hkdata )
  255. {
  256. const auto& changed_hk = hkdata->GetChangedHotkey();
  257. const EDA_HOTKEY& hk = changed_hk.GetCurrentValue();
  258. wxString key_text = KeyNameFromKeyCode( hk.m_KeyCode );
  259. // mark unsaved changes
  260. if( changed_hk.HasUnsavedChange() )
  261. key_text += " *";
  262. SetItemText( i, 0, wxGetTranslation( hk.m_InfoMsg ) );
  263. SetItemText( i, 1, key_text);
  264. }
  265. }
  266. // Trigger a resize in case column widths have changed
  267. wxSizeEvent dummy_evt;
  268. TWO_COLUMN_TREE_LIST::OnSize( dummy_evt );
  269. }
  270. void WIDGET_HOTKEY_LIST::changeHotkey( CHANGED_HOTKEY& aHotkey, long aKey )
  271. {
  272. // See if this key code is handled in hotkeys names list
  273. bool exists;
  274. KeyNameFromKeyCode( aKey, &exists );
  275. auto& curr_hk = aHotkey.GetCurrentValue();
  276. if( exists && curr_hk.m_KeyCode != aKey )
  277. {
  278. const auto& tag = aHotkey.GetSectionTag();
  279. bool can_update = ResolveKeyConflicts( aKey, tag );
  280. if( can_update )
  281. {
  282. curr_hk.m_KeyCode = aKey;
  283. }
  284. }
  285. }
  286. void WIDGET_HOTKEY_LIST::EditItem( wxTreeListItem aItem )
  287. {
  288. WIDGET_HOTKEY_CLIENT_DATA* hkdata = getExpectedHkClientData( aItem );
  289. if( !hkdata )
  290. return;
  291. wxString name = GetItemText( aItem, 0 );
  292. wxString current_key = GetItemText( aItem, 1 );
  293. wxKeyEvent key_event = HK_PROMPT_DIALOG::PromptForKey( GetParent(), name, current_key );
  294. long key = MapKeypressToKeycode( key_event );
  295. if( key )
  296. {
  297. changeHotkey( hkdata->GetChangedHotkey(), key );
  298. UpdateFromClientData();
  299. }
  300. }
  301. void WIDGET_HOTKEY_LIST::ResetItem( wxTreeListItem aItem )
  302. {
  303. WIDGET_HOTKEY_CLIENT_DATA* hkdata = getExpectedHkClientData( aItem );
  304. if( !hkdata )
  305. return;
  306. auto& changed_hk = hkdata->GetChangedHotkey();
  307. const auto& orig_hk = changed_hk.GetOriginalValue();
  308. changeHotkey( changed_hk, orig_hk.m_KeyCode );
  309. UpdateFromClientData();
  310. }
  311. void WIDGET_HOTKEY_LIST::ResetItemToDefault( wxTreeListItem aItem )
  312. {
  313. WIDGET_HOTKEY_CLIENT_DATA* hkdata = getExpectedHkClientData( aItem );
  314. if( !hkdata )
  315. return;
  316. auto& changed_hk = hkdata->GetChangedHotkey();
  317. changeHotkey( changed_hk, changed_hk.GetCurrentValue().GetDefaultKeyCode() );
  318. UpdateFromClientData();
  319. }
  320. void WIDGET_HOTKEY_LIST::OnActivated( wxTreeListEvent& aEvent )
  321. {
  322. EditItem( aEvent.GetItem() );
  323. }
  324. void WIDGET_HOTKEY_LIST::OnContextMenu( wxTreeListEvent& aEvent )
  325. {
  326. // Save the active event for use in OnMenu
  327. m_context_menu_item = aEvent.GetItem();
  328. wxMenu menu;
  329. WIDGET_HOTKEY_CLIENT_DATA* hkdata = GetHKClientData( m_context_menu_item );
  330. // Some actions only apply if the row is hotkey data
  331. if( hkdata )
  332. {
  333. menu.Append( ID_EDIT, _( "Edit..." ) );
  334. menu.Append( ID_RESET, _( "Undo Changes" ) );
  335. menu.Append( ID_DEFAULT, _( "Restore Default" ) );
  336. menu.Append( wxID_SEPARATOR );
  337. }
  338. menu.Append( ID_RESET_ALL, _( "Undo All Changes" ) );
  339. menu.Append( ID_DEFAULT_ALL, _( "Restore All to Default" ) );
  340. PopupMenu( &menu );
  341. }
  342. void WIDGET_HOTKEY_LIST::OnMenu( wxCommandEvent& aEvent )
  343. {
  344. switch( aEvent.GetId() )
  345. {
  346. case ID_EDIT:
  347. EditItem( m_context_menu_item );
  348. break;
  349. case ID_RESET:
  350. ResetItem( m_context_menu_item );
  351. break;
  352. case ID_DEFAULT:
  353. ResetItemToDefault( m_context_menu_item );
  354. break;
  355. case ID_RESET_ALL:
  356. ResetAllHotkeys( false );
  357. break;
  358. case ID_DEFAULT_ALL:
  359. ResetAllHotkeys( true );
  360. break;
  361. default:
  362. wxFAIL_MSG( wxT( "Unknown ID in context menu event" ) );
  363. }
  364. }
  365. bool WIDGET_HOTKEY_LIST::ResolveKeyConflicts( long aKey, const wxString& aSectionTag )
  366. {
  367. EDA_HOTKEY* conflicting_key = nullptr;
  368. EDA_HOTKEY_CONFIG* conflicting_section = nullptr;
  369. m_hk_store.CheckKeyConflicts( aKey, aSectionTag, &conflicting_key, &conflicting_section );
  370. if( conflicting_key != nullptr )
  371. {
  372. wxString info = wxGetTranslation( conflicting_key->m_InfoMsg );
  373. wxString msg = wxString::Format(
  374. _( "\"%s\" is already assigned to \"%s\" in section \"%s\". Are you sure you want "
  375. "to change its assignment?" ),
  376. KeyNameFromKeyCode( aKey ), GetChars( info ),
  377. *(conflicting_section->m_Title) );
  378. wxMessageDialog dlg( GetParent(), msg, _( "Confirm change" ), wxYES_NO | wxNO_DEFAULT );
  379. if( dlg.ShowModal() == wxID_YES )
  380. {
  381. // Reset the other hotkey
  382. conflicting_key->m_KeyCode = 0;
  383. UpdateFromClientData();
  384. return true;
  385. }
  386. else
  387. {
  388. return false;
  389. }
  390. }
  391. else
  392. {
  393. return true;
  394. }
  395. }
  396. WIDGET_HOTKEY_LIST::WIDGET_HOTKEY_LIST( wxWindow* aParent, HOTKEY_STORE& aHotkeyStore,
  397. bool aReadOnly )
  398. : TWO_COLUMN_TREE_LIST( aParent, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxTL_SINGLE ),
  399. m_hk_store( aHotkeyStore ),
  400. m_readOnly( aReadOnly )
  401. {
  402. wxString command_header = _( "Command" );
  403. if( !m_readOnly )
  404. command_header << " " << _( "(double-click to edit)" );
  405. AppendColumn( command_header );
  406. AppendColumn( _( "Hotkey" ) );
  407. SetRubberBandColumn( 0 );
  408. SetClampedMinWidth( HOTKEY_MIN_WIDTH );
  409. if( !m_readOnly )
  410. {
  411. // The event only apply if the widget is in editable mode
  412. Bind( wxEVT_TREELIST_ITEM_ACTIVATED, &WIDGET_HOTKEY_LIST::OnActivated, this );
  413. Bind( wxEVT_TREELIST_ITEM_CONTEXT_MENU, &WIDGET_HOTKEY_LIST::OnContextMenu, this );
  414. Bind( wxEVT_MENU, &WIDGET_HOTKEY_LIST::OnMenu, this );
  415. }
  416. }
  417. void WIDGET_HOTKEY_LIST::ApplyFilterString( const wxString& aFilterStr )
  418. {
  419. updateShownItems( aFilterStr );
  420. }
  421. void WIDGET_HOTKEY_LIST::ResetAllHotkeys( bool aResetToDefault )
  422. {
  423. Freeze();
  424. // Reset all the hotkeys, not just the ones shown
  425. // Should not need to check conflicts, as the state we're about
  426. // to set to a should be consistent
  427. if( aResetToDefault )
  428. {
  429. m_hk_store.ResetAllHotkeysToDefault();
  430. }
  431. else
  432. {
  433. m_hk_store.ResetAllHotkeysToOriginal();
  434. }
  435. UpdateFromClientData();
  436. Thaw();
  437. }
  438. bool WIDGET_HOTKEY_LIST::TransferDataToControl()
  439. {
  440. updateShownItems( "" );
  441. return true;
  442. }
  443. void WIDGET_HOTKEY_LIST::updateShownItems( const wxString& aFilterStr )
  444. {
  445. Freeze();
  446. DeleteAllItems();
  447. HOTKEY_FILTER filter( aFilterStr );
  448. for( auto& section: m_hk_store.GetSections() )
  449. {
  450. // Create parent tree item
  451. wxTreeListItem parent = AppendItem( GetRootItem(), section.m_name );
  452. for( auto& hotkey: section.m_hotkeys )
  453. {
  454. if( filter.FilterMatches( hotkey.GetCurrentValue() ) )
  455. {
  456. wxTreeListItem item = AppendItem( parent, wxEmptyString );
  457. SetItemData( item, new WIDGET_HOTKEY_CLIENT_DATA( hotkey ) );
  458. }
  459. }
  460. Expand( parent );
  461. }
  462. UpdateFromClientData();
  463. Thaw();
  464. }
  465. bool WIDGET_HOTKEY_LIST::TransferDataFromControl()
  466. {
  467. m_hk_store.SaveAllHotkeys();
  468. return true;
  469. }
  470. long WIDGET_HOTKEY_LIST::MapKeypressToKeycode( const wxKeyEvent& aEvent )
  471. {
  472. long key = aEvent.GetKeyCode();
  473. if( key == WXK_ESCAPE )
  474. {
  475. return 0;
  476. }
  477. else
  478. {
  479. if( key >= 'a' && key <= 'z' ) // convert to uppercase
  480. key = key + ('A' - 'a');
  481. // Remap Ctrl A (=1+GR_KB_CTRL) to Ctrl Z(=26+GR_KB_CTRL)
  482. // to GR_KB_CTRL+'A' .. GR_KB_CTRL+'Z'
  483. if( aEvent.ControlDown() && key >= WXK_CONTROL_A && key <= WXK_CONTROL_Z )
  484. key += 'A' - 1;
  485. /* Disallow shift for keys that have two keycodes on them (e.g. number and
  486. * punctuation keys) leaving only the "letter keys" of A-Z.
  487. * Then, you can have, e.g. Ctrl-5 and Ctrl-% (GB layout)
  488. * and Ctrl-( and Ctrl-5 (FR layout).
  489. * Otherwise, you'd have to have to say Ctrl-Shift-5 on a FR layout
  490. */
  491. bool keyIsLetter = key >= 'A' && key <= 'Z';
  492. if( aEvent.ShiftDown() && ( keyIsLetter || key > 256 ) )
  493. key |= GR_KB_SHIFT;
  494. if( aEvent.ControlDown() )
  495. key |= GR_KB_CTRL;
  496. if( aEvent.AltDown() )
  497. key |= GR_KB_ALT;
  498. return key;
  499. }
  500. }