949 lines
27 KiB
C++
949 lines
27 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 "KeyConfigPrefs.h"
|
|
|
|
#include <wx/setup.h> // for wxUSE_* macros
|
|
#include <wx/defs.h>
|
|
#include <wx/ffile.h>
|
|
#include <wx/intl.h>
|
|
#include <wx/menu.h>
|
|
#include <wx/button.h>
|
|
#include <wx/radiobut.h>
|
|
#include <wx/stattext.h>
|
|
#include <wx/statbox.h>
|
|
#include <wx/textctrl.h>
|
|
|
|
#include "../Prefs.h"
|
|
#include "../Project.h"
|
|
#include "../commands/CommandManager.h"
|
|
#include "../xml/XMLFileReader.h"
|
|
|
|
#include "../ShuttleGui.h"
|
|
|
|
#include "../FileNames.h"
|
|
|
|
#include "../widgets/KeyView.h"
|
|
#include "../widgets/AudacityMessageBox.h"
|
|
|
|
#if wxUSE_ACCESSIBILITY
|
|
#include "../widgets/WindowAccessible.h"
|
|
#endif
|
|
|
|
//
|
|
// 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
|
|
|
|
// EMPTY_SHORTCUT means "user chose to have no shortcut"
|
|
#define EMPTY_SHORTCUT ("")
|
|
// NO_SHORTCUT means "user made no choice"
|
|
#define NO_SHORTCUT (wxString)((wxChar)7)
|
|
|
|
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, wxWindowID winid, AudacityProject *pProject,
|
|
const CommandID &name)
|
|
/* i18n-hint: as in computer keyboard (not musical!) */
|
|
: PrefsPanel(parent, winid, XO("Keyboard")),
|
|
mView(NULL),
|
|
mKey(NULL),
|
|
mFilter(NULL),
|
|
mFilterTimer(this, FilterTimerID),
|
|
mFilterPending(false)
|
|
, mProject{ pProject }
|
|
{
|
|
Populate();
|
|
if (!name.empty()) {
|
|
auto index = mView->GetIndexByName(name);
|
|
mView->SelectNode(index);
|
|
}
|
|
|
|
// See bug #2315 for discussion. This should be reviewed
|
|
// and (possibly) removed after wx3.1.3.
|
|
Bind(wxEVT_SHOW, &KeyConfigPrefs::OnShow, this);
|
|
}
|
|
|
|
ComponentInterfaceSymbol KeyConfigPrefs::GetSymbol()
|
|
{
|
|
return KEY_CONFIG_PREFS_PLUGIN_SYMBOL;
|
|
}
|
|
|
|
TranslatableString KeyConfigPrefs::GetDescription()
|
|
{
|
|
return XO("Preferences for KeyConfig");
|
|
}
|
|
|
|
ManualPageID KeyConfigPrefs::HelpPageName()
|
|
{
|
|
return "Keyboard_Preferences";
|
|
}
|
|
|
|
void KeyConfigPrefs::Populate()
|
|
{
|
|
ShuttleGui S(this, eIsCreatingFromPrefs);
|
|
|
|
if (!mProject) {
|
|
S.StartVerticalLay(true);
|
|
{
|
|
S.StartStatic( {}, true);
|
|
{
|
|
S.AddTitle(XO("Keyboard preferences currently unavailable."));
|
|
S.AddTitle(XO("Open a new project to modify keyboard shortcuts."));
|
|
}
|
|
S.EndStatic();
|
|
}
|
|
S.EndVerticalLay();
|
|
|
|
return;
|
|
}
|
|
|
|
PopulateOrExchange(S);
|
|
|
|
mCommandSelected = wxNOT_FOUND;
|
|
|
|
mManager = &CommandManager::Get( *mProject );
|
|
|
|
// 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(XO("Key Bindings"), 1);
|
|
{
|
|
S.StartHorizontalLay(wxEXPAND, 0);
|
|
{
|
|
S.Position(wxALIGN_LEFT | wxALIGN_CENTER_VERTICAL).AddTitle(XO("View by:"));
|
|
|
|
// Bug 2692: Place button group in panel so tabbing will work and,
|
|
// on the Mac, VoiceOver will announce as radio buttons.
|
|
S.StartPanel();
|
|
{
|
|
S.StartHorizontalLay();
|
|
{
|
|
S.StartRadioButtonGroup({
|
|
wxT("/Prefs/KeyConfig/ViewBy"),
|
|
{
|
|
{ wxT("tree"), XXO("&Tree") },
|
|
{ wxT("name"), XXO("&Name") },
|
|
{ wxT("key"), XXO("&Key") },
|
|
},
|
|
0 // tree
|
|
});
|
|
{
|
|
mViewByTree = S.Id(ViewByTreeID)
|
|
.Name(XO("View by tree"))
|
|
.TieRadioButton();
|
|
mViewByName = S.Id(ViewByNameID)
|
|
.Name(XO("View by name"))
|
|
.TieRadioButton();
|
|
mViewByKey = S.Id(ViewByKeyID)
|
|
.Name(XO("View by key"))
|
|
.TieRadioButton();
|
|
#if !defined(__WXMAC__) && wxUSE_ACCESSIBILITY
|
|
// so that name can be set on a standard control
|
|
if (mViewByTree) mViewByTree->SetAccessible(safenew WindowAccessible(mViewByTree));
|
|
if (mViewByName) mViewByName->SetAccessible(safenew WindowAccessible(mViewByName));
|
|
if (mViewByKey) mViewByKey->SetAccessible(safenew WindowAccessible(mViewByKey));
|
|
#endif
|
|
}
|
|
S.EndRadioButtonGroup();
|
|
}
|
|
S.EndHorizontalLay();
|
|
}
|
|
S.EndPanel();
|
|
|
|
S.AddSpace(wxDefaultCoord, wxDefaultCoord, 1);
|
|
|
|
S.StartHorizontalLay(wxALIGN_CENTER_VERTICAL, 0);
|
|
{
|
|
mFilterLabel = S.Position(wxALIGN_CENTER_VERTICAL).AddVariableText(XO("Searc&h:"));
|
|
|
|
if (!mFilter) {
|
|
mFilter = safenew wxTextCtrl(S.GetParent(),
|
|
FilterID,
|
|
wxT(""),
|
|
wxDefaultPosition,
|
|
#if defined(__WXMAC__)
|
|
wxSize(300, -1),
|
|
#else
|
|
wxSize(210, -1),
|
|
#endif
|
|
wxTE_PROCESS_ENTER);
|
|
mFilter->SetName(wxStripMenuCodes(mFilterLabel->GetLabel()));
|
|
}
|
|
S.Position(wxALIGN_NOT | wxALIGN_LEFT)
|
|
.ConnectRoot(wxEVT_KEY_DOWN,
|
|
&KeyConfigPrefs::OnFilterKeyDown)
|
|
.ConnectRoot(wxEVT_CHAR,
|
|
&KeyConfigPrefs::OnFilterChar)
|
|
.AddWindow(mFilter);
|
|
}
|
|
S.EndHorizontalLay();
|
|
}
|
|
S.EndHorizontalLay();
|
|
|
|
S.AddSpace(wxDefaultCoord, 2);
|
|
|
|
S.StartHorizontalLay(wxEXPAND, 1);
|
|
{
|
|
if (!mView) {
|
|
mView = safenew KeyView(S.GetParent(), CommandsListID);
|
|
mView->SetName(_("Bindings"));
|
|
}
|
|
S.Prop(true)
|
|
.Position(wxEXPAND)
|
|
.AddWindow(mView);
|
|
}
|
|
S.EndHorizontalLay();
|
|
|
|
S.StartThreeColumn();
|
|
{
|
|
if (!mKey) {
|
|
mKey = safenew wxTextCtrl(S.GetParent(),
|
|
CurrentComboID,
|
|
wxT(""),
|
|
wxDefaultPosition,
|
|
#if defined(__WXMAC__)
|
|
wxSize(300, -1),
|
|
#else
|
|
wxSize(210, -1),
|
|
#endif
|
|
wxTE_PROCESS_ENTER);
|
|
#if !defined(__WXMAC__) && wxUSE_ACCESSIBILITY
|
|
// so that name can be set on a standard control
|
|
mKey->SetAccessible(safenew WindowAccessible(mKey));
|
|
#endif
|
|
mKey->SetName(_("Short cut"));
|
|
}
|
|
S
|
|
.ConnectRoot(wxEVT_KEY_DOWN,
|
|
&KeyConfigPrefs::OnHotkeyKeyDown)
|
|
.ConnectRoot(wxEVT_CHAR,
|
|
&KeyConfigPrefs::OnHotkeyChar)
|
|
.ConnectRoot(wxEVT_KILL_FOCUS,
|
|
&KeyConfigPrefs::OnHotkeyKillFocus)
|
|
.ConnectRoot(wxEVT_CONTEXT_MENU,
|
|
&KeyConfigPrefs::OnHotkeyContext)
|
|
.AddWindow(mKey);
|
|
|
|
/* i18n-hint: (verb)*/
|
|
mSet = S.Id(SetButtonID).AddButton(XXO("&Set"));
|
|
/* i18n-hint: (verb)*/
|
|
mClear = S.Id(ClearButtonID).AddButton(XXO("Cl&ear"));
|
|
}
|
|
S.EndThreeColumn();
|
|
|
|
#if defined(__WXMAC__)
|
|
S.AddFixedText(XO("Note: Pressing Cmd+Q will quit. All other keys are valid."));
|
|
#endif
|
|
|
|
S.StartThreeColumn();
|
|
{
|
|
S.Id(ImportButtonID).AddButton(XXO("&Import..."));
|
|
S.Id(ExportButtonID).AddButton(XXO("&Export..."));
|
|
S.Id(AssignDefaultsButtonID).AddButton(XXO("&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)
|
|
{
|
|
TranslatableStrings Labels;
|
|
TranslatableStrings Categories;
|
|
TranslatableStrings 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;
|
|
}
|
|
|
|
// RefreshKeyInfo is used to update mKeys vector only
|
|
// Introduced for efficiency purposes to avoid unnecessary usage of RefreshBinding
|
|
void KeyConfigPrefs::RefreshKeyInfo()
|
|
{
|
|
mKeys.clear();
|
|
|
|
for (const auto & name : mNames)
|
|
mKeys.push_back(mManager->GetKeyFromName(name));
|
|
}
|
|
|
|
// Removes all shortcuts
|
|
// Doesn't call RefreshBindings()
|
|
void KeyConfigPrefs::ClearAllKeys()
|
|
{
|
|
const NormalizedKeyString noKey{ NO_SHORTCUT };
|
|
for (const auto & command : mNames)
|
|
mManager->SetKeyFromName(command, noKey);
|
|
}
|
|
|
|
// Checks if the given vector of keys contains illegal duplicates.
|
|
// In case it does, stores the prefixed labels of operations
|
|
// with illegal key duplicates in fMatching and sMatching.
|
|
// Search for duplicates fully implemented here
|
|
// to avoid possible problems with legal shortcut duplicates.
|
|
bool KeyConfigPrefs::ContainsIllegalDups(
|
|
TranslatableString & fMatching, TranslatableString & sMatching) const
|
|
{
|
|
using IndexesArray = std::vector<int>;
|
|
std::unordered_map<NormalizedKeyString, IndexesArray> seen;
|
|
|
|
for (size_t i{ 0 }; i < mKeys.size(); i++)
|
|
{
|
|
if (mKeys[i] == EMPTY_SHORTCUT || mKeys[i] == NO_SHORTCUT)
|
|
continue;
|
|
|
|
if (seen.count(mKeys[i]) == 0)
|
|
seen.insert({ mKeys[i], {(int)i} });
|
|
else
|
|
{
|
|
IndexesArray checkMe{ seen.at(mKeys[i]) };
|
|
for (int index : checkMe)
|
|
{
|
|
if (mDefaultKeys[i] == EMPTY_SHORTCUT ||
|
|
mDefaultKeys[i] != mDefaultKeys[index])
|
|
{
|
|
fMatching = mManager->GetPrefixedLabelFromName(mNames[i]);
|
|
sMatching = mManager->GetPrefixedLabelFromName(mNames[index]);
|
|
return true;
|
|
}
|
|
else
|
|
seen.at(mKeys[i]).push_back(index);
|
|
}
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
|
|
// This function tries to add the given shortcuts(keys) "toAdd"
|
|
// to the already existing shortcuts(keys). Shortcuts are added only if
|
|
// 1. the shortcut for the operation isn't defined already
|
|
// 2. the added shortcut doesn't create illegal shortcut duplicate
|
|
// The names of operations for which the second condition was violated
|
|
// are returned in a single error message
|
|
TranslatableString KeyConfigPrefs::MergeWithExistingKeys(
|
|
const std::vector<NormalizedKeyString> &toAdd)
|
|
{
|
|
TranslatableString disabledShortcuts;
|
|
|
|
auto searchAddInKeys = [&](size_t index)
|
|
{
|
|
for (size_t k{ 0 }; k < toAdd.size(); k++)
|
|
if (k == index)
|
|
continue;
|
|
else if (toAdd[index] == mKeys[k] &&
|
|
(mDefaultKeys[k] == EMPTY_SHORTCUT ||
|
|
mDefaultKeys[k] != mDefaultKeys[index]))
|
|
return (int)k;
|
|
|
|
return -1;
|
|
};
|
|
|
|
const NormalizedKeyString noKey{ EMPTY_SHORTCUT };
|
|
|
|
for (size_t i{ 0 }; i < toAdd.size(); i++)
|
|
{
|
|
if (mKeys[i] != NO_SHORTCUT)
|
|
continue;
|
|
else if (toAdd[i] == EMPTY_SHORTCUT)
|
|
mManager->SetKeyFromIndex(i, noKey);
|
|
else
|
|
{
|
|
int sRes{ searchAddInKeys(i) };
|
|
|
|
if (sRes == -1)
|
|
mManager->SetKeyFromIndex(i, toAdd[i]);
|
|
else
|
|
{
|
|
TranslatableString name{ mManager->GetKeyFromName(mNames[sRes]).GET(), {} };
|
|
|
|
disabledShortcuts +=
|
|
XO(
|
|
"\n * \"%s\" (because the shortcut \'%s\' is used by \"%s\")\n")
|
|
.Format(
|
|
mManager->GetPrefixedLabelFromName(mNames[i]),
|
|
name,
|
|
mManager->GetPrefixedLabelFromName(mNames[sRes]) );
|
|
|
|
mManager->SetKeyFromIndex(i, noKey);
|
|
}
|
|
}
|
|
}
|
|
|
|
return disabledShortcuts;
|
|
}
|
|
|
|
// See bug #2315 for discussion. This should be reviewed
|
|
// and (possibly) removed after wx3.1.3.
|
|
void KeyConfigPrefs::OnShow(wxShowEvent & event)
|
|
{
|
|
event.Skip();
|
|
|
|
if (event.IsShown())
|
|
{
|
|
mView->Refresh();
|
|
}
|
|
}
|
|
|
|
void KeyConfigPrefs::OnImport(wxCommandEvent & WXUNUSED(event))
|
|
{
|
|
wxString file = wxT("Audacity-keys.xml");
|
|
|
|
file = FileNames::SelectFile(FileNames::Operation::Open,
|
|
XO("Select an XML file containing Audacity keyboard shortcuts..."),
|
|
wxEmptyString,
|
|
file,
|
|
wxT(""),
|
|
{ FileNames::XMLFiles, FileNames::AllFiles },
|
|
wxRESIZE_BORDER,
|
|
this);
|
|
|
|
if (!file) {
|
|
return;
|
|
}
|
|
|
|
// this RefreshKeyInfo is here to account for
|
|
// potential OnSet() function executions before importing
|
|
RefreshKeyInfo();
|
|
|
|
// saving pre-import settings
|
|
const std::vector<NormalizedKeyString> oldKeys{ mKeys };
|
|
|
|
// clearing all pre-import settings
|
|
ClearAllKeys();
|
|
|
|
// getting new settings
|
|
XMLFileReader reader;
|
|
if (!reader.Parse(mManager, file)) {
|
|
AudacityMessageBox(
|
|
reader.GetErrorStr(),
|
|
XO("Error Importing Keyboard Shortcuts"),
|
|
wxOK | wxCENTRE,
|
|
this);
|
|
}
|
|
|
|
RefreshKeyInfo();
|
|
|
|
// checking new setting for duplicates
|
|
// if there are duplicates, throwing error and returning to pre-import state
|
|
TranslatableString fMatching;
|
|
TranslatableString sMatching;
|
|
|
|
if (ContainsIllegalDups(fMatching, sMatching))
|
|
{
|
|
// restore the old pre-import hotkeys stored in oldKeys
|
|
for (size_t k{ 0 }; k < mNames.size(); k++)
|
|
mManager->SetKeyFromName(mNames[k], oldKeys[k]);
|
|
mKeys = oldKeys;
|
|
|
|
// output an error message
|
|
AudacityMessageBox(
|
|
XO(
|
|
"The file with the shortcuts contains illegal shortcut duplicates for \"%s\" and \"%s\".\nNothing is imported.")
|
|
.Format( fMatching, sMatching ),
|
|
XO("Error Importing Keyboard Shortcuts"),
|
|
wxICON_ERROR | wxCENTRE, this);
|
|
|
|
// stop the function
|
|
return;
|
|
}
|
|
|
|
// adding possible old settings to the new settings and recording the conflicting ones
|
|
TranslatableString disabledShortcuts{ MergeWithExistingKeys(oldKeys) };
|
|
|
|
RefreshBindings(true);
|
|
|
|
TranslatableString message{
|
|
XO("Loaded %d keyboard shortcuts\n").Format(mManager->GetNumberOfKeysRead()) };
|
|
|
|
if (disabledShortcuts.Translation() != (""))
|
|
message += XO("\nThe following commands are not mentioned in the imported file, "
|
|
"but have their shortcuts removed because of the conflict with other new shortcuts:\n") +
|
|
disabledShortcuts;
|
|
|
|
AudacityMessageBox(message, XO("Loading Keyboard Shortcuts"), wxOK | wxCENTRE);
|
|
}
|
|
|
|
void KeyConfigPrefs::OnExport(wxCommandEvent & WXUNUSED(event))
|
|
{
|
|
wxString file = wxT("Audacity-keys.xml");
|
|
|
|
file = FileNames::SelectFile(FileNames::Operation::Export,
|
|
XO("Export Keyboard Shortcuts As:"),
|
|
wxEmptyString,
|
|
file,
|
|
wxT("xml"),
|
|
{ FileNames::XMLFiles, FileNames::AllFiles },
|
|
wxFD_SAVE | wxFD_OVERWRITE_PROMPT | wxRESIZE_BORDER,
|
|
this);
|
|
|
|
if (!file) {
|
|
return;
|
|
}
|
|
|
|
GuardedCall( [&] {
|
|
XMLFileWriter prefFile{ file, XO("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( std::vector<NormalizedKeyString> & arr )
|
|
{
|
|
const auto &MaxListOnly = CommandManager::ExcludedList();
|
|
|
|
// Remove items that are in MaxList.
|
|
for (size_t i = 0; i < arr.size(); i++) {
|
|
if( std::binary_search(MaxListOnly.begin(), MaxListOnly.end(), arr[i]) )
|
|
arr[i] = {};
|
|
}
|
|
}
|
|
|
|
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.size(); 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) {
|
|
t->Navigate(e.ShiftDown()
|
|
? wxNavigationKeyEvent::IsBackward
|
|
: wxNavigationKeyEvent::IsForward);
|
|
return;
|
|
}
|
|
|
|
t->SetValue(KeyEventToKeyString(e).Display());
|
|
}
|
|
|
|
void KeyConfigPrefs::OnHotkeyChar(wxEvent & WXUNUSED(e))
|
|
{
|
|
// event.Skip() not performed, so event will not be processed further.
|
|
}
|
|
|
|
void KeyConfigPrefs::OnHotkeyKillFocus(wxEvent & e)
|
|
{
|
|
if (mKey->GetValue().empty() && mCommandSelected != wxNOT_FOUND) {
|
|
mKey->AppendText(mView->GetKey(mCommandSelected).Display());
|
|
}
|
|
|
|
e.Skip();
|
|
}
|
|
|
|
void KeyConfigPrefs::OnHotkeyContext(wxEvent & WXUNUSED(e))
|
|
{
|
|
// event.Skip() not performed, so event will not be processed further.
|
|
}
|
|
|
|
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 = KeyEventToKeyString(e).Display();
|
|
t->SetValue(key);
|
|
|
|
if (!key.empty()) {
|
|
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(wxEvent & 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.
|
|
CommandID KeyConfigPrefs::NameFromKey(const NormalizedKeyString & 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 NormalizedKeyString & key)
|
|
{
|
|
auto name = mView->GetName(mCommandSelected);
|
|
|
|
if (!mView->CanSetKey(mCommandSelected))
|
|
{
|
|
AudacityMessageBox(
|
|
XO("You may not assign a key to this entry"),
|
|
XO("Error"),
|
|
wxICON_ERROR | wxCENTRE,
|
|
this);
|
|
return;
|
|
}
|
|
|
|
mView->SetKey(mCommandSelected, key);
|
|
mManager->SetKeyFromName(name, key);
|
|
mNewKeys[ make_iterator_range( mNames ).index( name ) ] = key;
|
|
}
|
|
|
|
|
|
void KeyConfigPrefs::OnSet(wxCommandEvent & WXUNUSED(event))
|
|
{
|
|
if (mCommandSelected == wxNOT_FOUND) {
|
|
AudacityMessageBox(
|
|
XO("You must select a binding before assigning a shortcut"),
|
|
XO("Error"),
|
|
wxICON_WARNING | wxCENTRE,
|
|
this);
|
|
return;
|
|
}
|
|
|
|
CommandID newCommand{ mView->GetName(mCommandSelected) };
|
|
NormalizedKeyString enteredKey{ mKey->GetValue() };
|
|
NormalizedKeyString newComDefaultKey{
|
|
mManager->GetDefaultKeyFromName(newCommand) };
|
|
CommandIDs oldCommands;
|
|
|
|
// collecting commands competing for the same shortcut
|
|
for (size_t i{ 0 }; i < mNames.size(); i++)
|
|
{
|
|
if (mNewKeys[i] == enteredKey)
|
|
{
|
|
// ignore the Set button if the same shortcut is used
|
|
if (mNames[i] == newCommand)
|
|
return;
|
|
|
|
if (newComDefaultKey == EMPTY_SHORTCUT ||
|
|
mDefaultKeys[i] != newComDefaultKey)
|
|
{
|
|
oldCommands.push_back(mNames[i]);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Prevent same hotkey combination being used twice.
|
|
if (!oldCommands.empty()) {
|
|
auto newlabel = Verbatim( wxT("'%s - %s'") )
|
|
.Format(
|
|
mManager->GetCategoryFromName(newCommand),
|
|
mManager->GetPrefixedLabelFromName(newCommand) );
|
|
auto oldlabel = Verbatim(wxT("'%s - %s'"))
|
|
.Format(
|
|
mManager->GetCategoryFromName(oldCommands[0]),
|
|
mManager->GetPrefixedLabelFromName(oldCommands[0]));
|
|
|
|
for (size_t i{ 1 }; i < oldCommands.size(); i++)
|
|
oldlabel += XO("\n\n\t and\n\n\t") +
|
|
Verbatim(wxT("'%s - %s'")).Format(
|
|
mManager->GetCategoryFromName(oldCommands[i]),
|
|
mManager->GetPrefixedLabelFromName(oldCommands[i]));
|
|
|
|
if (wxCANCEL == AudacityMessageBox(
|
|
XO(
|
|
"The keyboard shortcut '%s' is already assigned to:\n\n\t%s\n\n\nClick OK to assign the shortcut to\n\n\t%s\n\ninstead. Otherwise, click Cancel.")
|
|
.Format(
|
|
mKey->GetValue(),
|
|
oldlabel,
|
|
newlabel
|
|
),
|
|
XO("Warning"),
|
|
wxOK | wxCANCEL | wxICON_STOP | wxCENTRE,
|
|
this))
|
|
{
|
|
return;
|
|
}
|
|
|
|
for (const auto & command : oldCommands)
|
|
{
|
|
mView->SetKeyByName(command, {});
|
|
mManager->SetKeyFromName(command, {});
|
|
mNewKeys[make_iterator_range(mNames).index(command)] = {};
|
|
}
|
|
}
|
|
|
|
SetKeyForSelected(enteredKey);
|
|
}
|
|
|
|
void KeyConfigPrefs::OnClear(wxCommandEvent& WXUNUSED(event))
|
|
{
|
|
mKey->Clear();
|
|
|
|
if (mCommandSelected != wxNOT_FOUND) {
|
|
SetKeyForSelected({});
|
|
}
|
|
}
|
|
|
|
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).Display());
|
|
}
|
|
|
|
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 available
|
|
// 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 ( !mProject ) {
|
|
return true;
|
|
}
|
|
|
|
ShuttleGui S(this, eIsSavingToPrefs);
|
|
PopulateOrExchange(S);
|
|
|
|
bool bFull = gPrefs->ReadBool(wxT("/GUI/Shortcuts/FullDefaults"), false);
|
|
for (size_t i = 0; i < mNames.size(); i++) {
|
|
const auto &dkey = bFull ? mDefaultKeys[i] : mStandardDefaultKeys[i];
|
|
// using GET to interpret CommandID as a config path component
|
|
auto name = wxT("/NewKeys/") + mNames[i].GET();
|
|
const auto &key = mNewKeys[i];
|
|
|
|
if (gPrefs->HasEntry(name)) {
|
|
if (key != NormalizedKeyString{ gPrefs->ReadObject(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.size(); i++) {
|
|
mManager->SetKeyFromIndex(i, mKeys[i]);
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
PrefsPanel::Factory
|
|
KeyConfigPrefsFactory( const CommandID &name )
|
|
{
|
|
return [=](wxWindow *parent, wxWindowID winid, AudacityProject *pProject)
|
|
{
|
|
wxASSERT(parent); // to justify safenew
|
|
auto result = safenew KeyConfigPrefs{ parent, winid, pProject, name };
|
|
return result;
|
|
};
|
|
}
|
|
namespace{
|
|
PrefsPanel::Registration sAttachment{ "KeyConfig",
|
|
KeyConfigPrefsFactory()
|
|
};
|
|
}
|