audacia/src/prefs/KeyConfigPrefs.cpp

756 lines
21 KiB
C++

/**********************************************************************
Audacity: A Digital Audio Editor
KeyConfigPrefs.cpp
Brian Gunlogson
Dominic Mazzoni
James Crook
*******************************************************************//*!
\class KeyConfigPrefs
\brief A PrefsPanel for keybindings.
The code for displaying keybindings is similar to code in MousePrefs.
It would be nice to create a NEW 'Bindings' class which both
KeyConfigPrefs and MousePrefs use.
*//*********************************************************************/
#include "../Audacity.h"
#include "../Experimental.h"
#include "KeyConfigPrefs.h"
#include <wx/defs.h>
#include <wx/ffile.h>
#include <wx/intl.h>
#include <wx/filedlg.h>
#include <wx/button.h>
#include "../Prefs.h"
#include "../Project.h"
#include "../commands/CommandManager.h"
#include "../commands/Keyboard.h"
#include "../xml/XMLFileReader.h"
#include "../Internat.h"
#include "../ShuttleGui.h"
#include "../FileNames.h"
#include "../widgets/KeyView.h"
#include "../widgets/ErrorDialog.h"
//
// KeyConfigPrefs
//
#define AssignDefaultsButtonID 17001
#define CurrentComboID 17002
#define SetButtonID 17003
#define ClearButtonID 17004
#define CommandsListID 17005
#define ExportButtonID 17006
#define ImportButtonID 17007
#define FilterID 17008
#define ViewByTreeID 17009
#define ViewByNameID 17010
#define ViewByKeyID 17011
#define FilterTimerID 17012
BEGIN_EVENT_TABLE(KeyConfigPrefs, PrefsPanel)
EVT_BUTTON(AssignDefaultsButtonID, KeyConfigPrefs::OnDefaults)
EVT_BUTTON(SetButtonID, KeyConfigPrefs::OnSet)
EVT_BUTTON(ClearButtonID, KeyConfigPrefs::OnClear)
EVT_BUTTON(ExportButtonID, KeyConfigPrefs::OnExport)
EVT_BUTTON(ImportButtonID, KeyConfigPrefs::OnImport)
EVT_LISTBOX(CommandsListID, KeyConfigPrefs::OnSelected)
EVT_RADIOBUTTON(ViewByTreeID, KeyConfigPrefs::OnViewBy)
EVT_RADIOBUTTON(ViewByNameID, KeyConfigPrefs::OnViewBy)
EVT_RADIOBUTTON(ViewByKeyID, KeyConfigPrefs::OnViewBy)
EVT_TIMER(FilterTimerID, KeyConfigPrefs::OnFilterTimer)
END_EVENT_TABLE()
KeyConfigPrefs::KeyConfigPrefs(wxWindow * parent, const wxString &name)
/* i18n-hint: as in computer keyboard (not musical!) */
: PrefsPanel(parent, _("Keyboard")),
mView(NULL),
mKey(NULL),
mFilter(NULL),
mFilterTimer(this, FilterTimerID),
mFilterPending(false)
{
Populate();
if (!name.empty()) {
auto index = mView->GetIndexByName(name);
mView->SelectNode(index);
}
}
KeyConfigPrefs::~KeyConfigPrefs()
{
if (mKey)
{
mKey->Disconnect(wxEVT_KEY_DOWN,
wxKeyEventHandler(KeyConfigPrefs::OnHotkeyKeyDown),
NULL,
this);
mKey->Disconnect(wxEVT_CHAR,
wxKeyEventHandler(KeyConfigPrefs::OnHotkeyChar),
NULL,
this);
mKey->Disconnect(wxEVT_KILL_FOCUS,
wxFocusEventHandler(KeyConfigPrefs::OnHotkeyKillFocus),
NULL,
this);
}
if (mFilter)
{
mKey->Disconnect(wxEVT_KEY_DOWN,
wxKeyEventHandler(KeyConfigPrefs::OnFilterKeyDown),
NULL,
this);
mKey->Disconnect(wxEVT_CHAR,
wxKeyEventHandler(KeyConfigPrefs::OnFilterChar),
NULL,
this);
}
}
void KeyConfigPrefs::Populate()
{
ShuttleGui S(this, eIsCreatingFromPrefs);
AudacityProject *project = GetActiveProject();
if (!project) {
S.StartVerticalLay(true);
{
S.StartStatic( {}, true);
{
S.AddTitle(_("Keyboard preferences currently unavailable."));
S.AddTitle(_("Open a new project to modify keyboard shortcuts."));
}
S.EndStatic();
}
S.EndVerticalLay();
return;
}
PopulateOrExchange(S);
mCommandSelected = wxNOT_FOUND;
mManager = project->GetCommandManager();
// For speed, don't sort here. We're just creating.
// Instead sort when we do SetView later in this function.
RefreshBindings(false);
if (mViewByTree->GetValue()) {
mViewType = ViewByTree;
}
else if (mViewByName->GetValue()) {
mViewType = ViewByName;
}
else if (mViewByKey->GetValue()) {
mViewType = ViewByKey;
mFilterLabel->SetLabel(_("&Hotkey:"));
mFilter->SetName(wxStripMenuCodes(mFilterLabel->GetLabel()));
}
mView->SetView(mViewType);
}
/// Normally in classes derived from PrefsPanel this function
/// is used both to populate the panel and to exchange data with it.
/// With KeyConfigPrefs all the exchanges are handled specially,
/// so this is only used in populating the panel.
void KeyConfigPrefs::PopulateOrExchange(ShuttleGui & S)
{
S.SetBorder(2);
S.StartStatic(_("Key Bindings"), 1);
{
S.StartMultiColumn(3, wxEXPAND);
{
S.SetStretchyCol(1);
S.StartHorizontalLay(wxALIGN_LEFT|wxALIGN_CENTER_VERTICAL, 0);
{
S.AddTitle(_("View by:"));
S.StartRadioButtonGroup(wxT("/Prefs/KeyConfig/ViewBy"), wxT("tree"));
{
mViewByTree = S.Id(ViewByTreeID).TieRadioButton(_("&Tree"), wxT("tree"));
mViewByTree->SetName(_("View by tree"));
mViewByName = S.Id(ViewByNameID).TieRadioButton(_("&Name"), wxT("name"));
mViewByName->SetName(_("View by name"));
mViewByKey = S.Id(ViewByKeyID).TieRadioButton(_("&Key"), wxT("key"));
mViewByKey->SetName(_("View by key"));
}
S.EndRadioButtonGroup();
}
S.EndHorizontalLay();
S.StartHorizontalLay(wxALIGN_CENTER|wxALIGN_CENTER_VERTICAL, 0);
{
// just a spacer
}
S.EndHorizontalLay();
S.StartHorizontalLay(wxALIGN_RIGHT|wxALIGN_CENTER_VERTICAL, 0);
{
mFilterLabel = S.AddVariableText(_("Searc&h:"));
if (!mFilter) {
mFilter = safenew wxTextCtrl(this,
FilterID,
wxT(""),
wxDefaultPosition,
#if defined(__WXMAC__)
wxSize(300, -1),
#else
wxSize(210, -1),
#endif
wxTE_PROCESS_ENTER);
mFilter->SetName(wxStripMenuCodes(mFilterLabel->GetLabel()));
mFilter->Connect(wxEVT_KEY_DOWN,
wxKeyEventHandler(KeyConfigPrefs::OnFilterKeyDown),
NULL,
this);
mFilter->Connect(wxEVT_CHAR,
wxKeyEventHandler(KeyConfigPrefs::OnFilterChar),
NULL,
this);
}
S.AddWindow(mFilter, wxALIGN_NOT | wxALIGN_LEFT);
}
S.EndHorizontalLay();
}
S.EndThreeColumn();
S.AddSpace(-1, 2);
S.StartHorizontalLay(wxEXPAND, 1);
{
if (!mView) {
mView = safenew KeyView(this, CommandsListID);
mView->SetName(_("Bindings"));
}
S.Prop(true);
S.AddWindow(mView, wxEXPAND);
}
S.EndHorizontalLay();
S.StartThreeColumn();
{
if (!mKey) {
mKey = safenew wxTextCtrl(this,
CurrentComboID,
wxT(""),
wxDefaultPosition,
#if defined(__WXMAC__)
wxSize(300, -1),
#else
wxSize(210, -1),
#endif
wxTE_PROCESS_ENTER);
mKey->SetName(_("Short cut"));
mKey->Connect(wxEVT_KEY_DOWN,
wxKeyEventHandler(KeyConfigPrefs::OnHotkeyKeyDown),
NULL,
this);
mKey->Connect(wxEVT_CHAR,
wxKeyEventHandler(KeyConfigPrefs::OnHotkeyChar),
NULL,
this);
mKey->Connect(wxEVT_KILL_FOCUS,
wxFocusEventHandler(KeyConfigPrefs::OnHotkeyKillFocus),
NULL,
this);
}
S.AddWindow(mKey);
/* i18n-hint: (verb)*/
mSet = S.Id(SetButtonID).AddButton(_("&Set"));
mClear = S.Id(ClearButtonID).AddButton(_("Cl&ear"));
}
S.EndThreeColumn();
#if defined(__WXMAC__)
S.AddFixedText(_("Note: Pressing Cmd+Q will quit. All other keys are valid."));
#endif
S.StartThreeColumn();
{
S.Id(ImportButtonID).AddButton(_("&Import..."));
S.Id(ExportButtonID).AddButton(_("&Export..."));
S.Id(AssignDefaultsButtonID).AddButton(_("&Defaults"));
}
S.EndThreeColumn();
}
S.EndStatic();
// Need to layout so that the KeyView is properly sized before populating.
// Otherwise, the initial selection is not scrolled into view.
Layout();
}
void KeyConfigPrefs::RefreshBindings(bool bSort)
{
wxArrayString Labels;
wxArrayString Categories;
wxArrayString Prefixes;
mNames.Clear();
mKeys.Clear();
mDefaultKeys.Clear();
mStandardDefaultKeys.Clear();
mManager->GetAllCommandData(
mNames,
mKeys,
mDefaultKeys,
Labels,
Categories,
Prefixes,
true); // True to include effects (list items), false otherwise.
mStandardDefaultKeys = mDefaultKeys;
FilterKeys( mStandardDefaultKeys );
mView->RefreshBindings(mNames,
Categories,
Prefixes,
Labels,
mKeys,
bSort);
//Not needed as NEW nodes are already shown expanded.
//mView->ExpandAll();
mNewKeys = mKeys;
}
void KeyConfigPrefs::OnImport(wxCommandEvent & WXUNUSED(event))
{
wxString file = wxT("Audacity-keys.xml");
file = FileNames::SelectFile(FileNames::Operation::Open,
_("Select an XML file containing Audacity keyboard shortcuts..."),
wxEmptyString,
file,
wxT(""),
_("XML files (*.xml)|*.xml|All files|*"),
wxRESIZE_BORDER,
this);
if (!file) {
return;
}
XMLFileReader reader;
if (!reader.Parse(mManager, file)) {
AudacityMessageBox(reader.GetErrorStr(),
_("Error Importing Keyboard Shortcuts"),
wxOK | wxCENTRE, this);
}
RefreshBindings(true);
}
void KeyConfigPrefs::OnExport(wxCommandEvent & WXUNUSED(event))
{
wxString file = wxT("Audacity-keys.xml");
file = FileNames::SelectFile(FileNames::Operation::Export,
_("Export Keyboard Shortcuts As:"),
wxEmptyString,
file,
wxT("xml"),
_("XML files (*.xml)|*.xml|All files|*"),
wxFD_SAVE | wxFD_OVERWRITE_PROMPT | wxRESIZE_BORDER,
this);
if (!file) {
return;
}
GuardedCall( [&] {
XMLFileWriter prefFile{ file, _("Error Exporting Keyboard Shortcuts") };
mManager->WriteXML(prefFile);
prefFile.Commit();
} );
}
// There currently is only one clickable AButton
// so we just do what it needs.
void KeyConfigPrefs::OnDefaults(wxCommandEvent & WXUNUSED(event))
{
wxMenu Menu;
Menu.Append( 1, _("Standard") );
Menu.Append( 2, _("Full") );
Menu.Bind( wxEVT_COMMAND_MENU_SELECTED, &KeyConfigPrefs::OnImportDefaults, this );
// Pop it up where the mouse is.
PopupMenu(&Menu);//, wxPoint(0, 0));
}
void KeyConfigPrefs::FilterKeys( wxArrayString & arr )
{
wxSortedArrayString MaxListOnly;
// These short cuts are for the max list only....
//MaxListOnly.Add( "Ctrl+I" );
MaxListOnly.Add( "Ctrl+Alt+I" );
MaxListOnly.Add( "Ctrl+J" );
MaxListOnly.Add( "Ctrl+Alt+J" );
MaxListOnly.Add( "Ctrl+Alt+V" );
MaxListOnly.Add( "Alt+X" );
MaxListOnly.Add( "Alt+K" );
MaxListOnly.Add( "Shift+Alt+X" );
MaxListOnly.Add( "Shift+Alt+K" );
MaxListOnly.Add( "Alt+L" );
MaxListOnly.Add( "Shift+Alt+C" );
MaxListOnly.Add( "Alt+I" );
MaxListOnly.Add( "Alt+J" );
MaxListOnly.Add( "Shift+Alt+J" );
MaxListOnly.Add( "Ctrl+Shift+A" );
MaxListOnly.Add( "Q" );
//MaxListOnly.Add( "Shift+J" );
//MaxListOnly.Add( "Shift+K" );
//MaxListOnly.Add( "Shift+Home" );
//MaxListOnly.Add( "Shift+End" );
MaxListOnly.Add( "Ctrl+[" );
MaxListOnly.Add( "Ctrl+]" );
MaxListOnly.Add( "1" );
MaxListOnly.Add( "Shift+F5" );
MaxListOnly.Add( "Shift+F6" );
MaxListOnly.Add( "Shift+F7" );
MaxListOnly.Add( "Shift+F8" );
MaxListOnly.Add( "Ctrl+Shift+F5" );
MaxListOnly.Add( "Ctrl+Shift+F7" );
MaxListOnly.Add( "Ctrl+Shift+N" );
MaxListOnly.Add( "Ctrl+Shift+M" );
MaxListOnly.Add( "Ctrl+Home" );
MaxListOnly.Add( "Ctrl+End" );
MaxListOnly.Add( "Shift+C" );
MaxListOnly.Add( "Alt+Shift+Up" );
MaxListOnly.Add( "Alt+Shift+Down" );
MaxListOnly.Add( "Shift+P" );
MaxListOnly.Add( "Alt+Shift+Left" );
MaxListOnly.Add( "Alt+Shift+Right" );
MaxListOnly.Add( "Ctrl+Shift+T" );
//MaxListOnly.Add( "Command+M" );
//MaxListOnly.Add( "Option+Command+M" );
MaxListOnly.Add( "Shift+H" );
MaxListOnly.Add( "Shift+O" );
MaxListOnly.Add( "Shift+I" );
MaxListOnly.Add( "Shift+N" );
MaxListOnly.Add( "D" );
MaxListOnly.Add( "A" );
MaxListOnly.Add( "Alt+Shift+F6" );
MaxListOnly.Add( "Alt+F6" );
MaxListOnly.Sort();
// Remove items that are in MaxList.
for (size_t i = 0; i < arr.GetCount(); i++) {
if( MaxListOnly.Index( arr[i] ) != wxNOT_FOUND )
arr[i]= wxT("");
}
}
void KeyConfigPrefs::OnImportDefaults(wxCommandEvent & event)
{
gPrefs->DeleteEntry(wxT("/GUI/Shortcuts/FullDefaults"));
gPrefs->Flush();
mNewKeys = mDefaultKeys;
if( event.GetId() == 1 )
FilterKeys( mNewKeys );
for (size_t i = 0; i < mNewKeys.GetCount(); i++) {
mManager->SetKeyFromIndex(i, mNewKeys[i]);
}
RefreshBindings(true);
}
void KeyConfigPrefs::OnHotkeyKeyDown(wxKeyEvent & e)
{
wxTextCtrl *t = (wxTextCtrl *)e.GetEventObject();
// Make sure we can navigate away from the hotkey textctrl.
// On Linux and OSX, it can get stuck, but it doesn't hurt
// to do it for Windows as well.
//
// Mac note: Don't waste time trying to figure out why the
// focus goes back to the prefs tree. Unless Voiceover is
// active, buttons on the Mac do not accept focus and all the
// controls between this one and the tree control are buttons.
if (e.GetKeyCode() == WXK_TAB) {
NavigateIn(e.ShiftDown()
? wxNavigationKeyEvent::IsBackward
: wxNavigationKeyEvent::IsForward);
return;
}
t->SetValue(KeyStringDisplay(KeyEventToKeyString(e)));
}
void KeyConfigPrefs::OnHotkeyChar(wxKeyEvent & WXUNUSED(e))
{
// event.Skip() not performed, so event will not be processed further.
}
void KeyConfigPrefs::OnHotkeyKillFocus(wxFocusEvent & e)
{
if (mKey->GetValue().IsEmpty() && mCommandSelected != wxNOT_FOUND) {
mKey->AppendText(mView->GetKey(mCommandSelected));
}
e.Skip();
}
void KeyConfigPrefs::OnFilterTimer(wxTimerEvent & WXUNUSED(e))
{
// The filter timer has expired, so set the filter
if (mFilterPending)
{
// Do not reset mFilterPending here...possible race
mView->SetFilter(mFilter->GetValue());
}
}
void KeyConfigPrefs::OnFilterKeyDown(wxKeyEvent & e)
{
wxTextCtrl *t = (wxTextCtrl *)e.GetEventObject();
int keycode = e.GetKeyCode();
// Make sure we can navigate away from the hotkey textctrl.
// On Linux and OSX, it an get stuck, but it doesn't hurt
// to do it for Windows as well.
if (keycode == WXK_TAB) {
wxNavigationKeyEvent nevent;
nevent.SetWindowChange(e.ControlDown());
nevent.SetDirection(!e.ShiftDown());
nevent.SetEventObject(t);
nevent.SetCurrentFocus(t);
t->GetParent()->GetEventHandler()->ProcessEvent(nevent);
return;
}
if (mViewType == ViewByKey) {
wxString key = KeyStringDisplay(KeyEventToKeyString(e));
t->SetValue(key);
if (key != wxEmptyString) {
mView->SetFilter(t->GetValue());
}
}
else
{
if (keycode == WXK_RETURN) {
mFilterPending = false;
mView->SetFilter(t->GetValue());
}
else {
mFilterPending = true;
mFilterTimer.Start(500, wxTIMER_ONE_SHOT);
e.Skip();
}
}
}
void KeyConfigPrefs::OnFilterChar(wxKeyEvent & e)
{
if (mViewType != ViewByKey)
{
e.Skip();
}
}
// Given a hotkey combination, returns the name (description) of the
// corresponding command, or the empty string if none is found.
wxString KeyConfigPrefs::NameFromKey(const wxString & key)
{
return mView->GetNameByKey(key);
}
// Sets the selected command to have this key
// This is not yet a committed change, which will happen on a save.
void KeyConfigPrefs::SetKeyForSelected(const wxString & key)
{
wxString name = mView->GetName(mCommandSelected);
if (!mView->CanSetKey(mCommandSelected))
{
AudacityMessageBox(_("You may not assign a key to this entry"),
_("Error"), wxICON_ERROR | wxCENTRE, this);
return;
}
mView->SetKey(mCommandSelected, key);
mManager->SetKeyFromName(name, key);
mNewKeys[mNames.Index(name)] = key;
}
void KeyConfigPrefs::OnSet(wxCommandEvent & WXUNUSED(event))
{
if (mCommandSelected == wxNOT_FOUND) {
AudacityMessageBox(_("You must select a binding before assigning a shortcut"),
_("Error"), wxICON_WARNING | wxCENTRE, this);
return;
}
wxString key = mKey->GetValue();
wxString oldname = mView->GetNameByKey(key);
wxString newname = mView->GetName(mCommandSelected);
// Just ignore it if they are the same
if (oldname == newname) {
return;
}
// Prevent same hotkey combination being used twice.
if (!oldname.empty()) {
auto oldlabel = wxString::Format( _("%s - %s"),
mManager->GetCategoryFromName(oldname),
mManager->GetPrefixedLabelFromName(oldname) );
auto newlabel = wxString::Format( _("%s - %s"),
mManager->GetCategoryFromName(newname),
mManager->GetPrefixedLabelFromName(newname) );
if (AudacityMessageBox(
wxString::Format(
_("The keyboard shortcut '%s' is already assigned to:\n\n\t'%s'\n\nClick OK to assign the shortcut to\n\n\t'%s'\n\ninstead. Otherwise, click Cancel."),
key,
oldlabel,
newlabel),
_("Error"), wxOK | wxCANCEL | wxICON_STOP | wxCENTRE, this) == wxCANCEL)
{
return;
}
mView->SetKeyByName(oldname, wxEmptyString);
mManager->SetKeyFromName(oldname, wxEmptyString);
mNewKeys[mNames.Index(oldname)].Empty();
}
SetKeyForSelected(key);
}
void KeyConfigPrefs::OnClear(wxCommandEvent& WXUNUSED(event))
{
mKey->Clear();
if (mCommandSelected != wxNOT_FOUND) {
SetKeyForSelected(wxEmptyString);
}
}
void KeyConfigPrefs::OnSelected(wxCommandEvent & WXUNUSED(e))
{
mCommandSelected = mView->GetSelected();
mKey->Clear();
if (mCommandSelected != wxNOT_FOUND) {
bool canset = mView->CanSetKey(mCommandSelected);
if (canset) {
mKey->AppendText(mView->GetKey(mCommandSelected));
}
mKey->Enable(canset);
mSet->Enable(canset);
mClear->Enable(canset);
}
}
void KeyConfigPrefs::OnViewBy(wxCommandEvent & e)
{
switch (e.GetId())
{
case ViewByTreeID:
mViewType = ViewByTree;
mFilterLabel->SetLabel(_("Searc&h:"));
break;
case ViewByNameID:
mViewType = ViewByName;
mFilterLabel->SetLabel(_("Searc&h:"));
break;
case ViewByKeyID:
mViewType = ViewByKey;
mFilterLabel->SetLabel(_("&Hotkey:"));
break;
}
mView->SetView(mViewType);
mFilter->SetName(wxStripMenuCodes(mFilterLabel->GetLabel()));
}
bool KeyConfigPrefs::Commit()
{
// On the Mac, preferences may be changed without any active
// projects. This means that the CommandManager isn't availabe
// either. So we can't attempt to save preferences, otherwise
// NULL ptr dereferences will happen in ShuttleGui because the
// radio buttons are never created. (See Populate() above.)
if (!GetActiveProject()) {
return true;
}
ShuttleGui S(this, eIsSavingToPrefs);
PopulateOrExchange(S);
for (size_t i = 0; i < mNames.GetCount(); i++) {
wxString dkey = KeyStringNormalize(mStandardDefaultKeys[i]);
wxString name = wxT("/NewKeys/") + mNames[i];
wxString key = KeyStringNormalize(mNewKeys[i]);
if (gPrefs->HasEntry(name)) {
if (key != KeyStringNormalize(gPrefs->Read(name, key))) {
gPrefs->Write(name, key);
}
if (key == dkey) {
gPrefs->DeleteEntry(name);
}
}
else {
if (key != dkey) {
gPrefs->Write(name, key);
}
}
}
return gPrefs->Flush();
}
void KeyConfigPrefs::Cancel()
{
// Restore original key values
for (size_t i = 0; i < mNames.GetCount(); i++) {
mManager->SetKeyFromIndex(i, mKeys[i]);
}
return;
}
wxString KeyConfigPrefs::HelpPageName()
{
return "Keyboard_Preferences";
}
PrefsPanel *KeyConfigPrefsFactory::Create(wxWindow *parent)
{
wxASSERT(parent); // to justify safenew
auto result = safenew KeyConfigPrefs{ parent, mName };
return result;
}