1
0
mirror of https://github.com/cookiengineer/audacity synced 2026-03-10 16:35:32 +01:00
Files
audacity/src/prefs/KeyConfigPrefs.cpp
David Bailes a1c03d0f7d Bug 1786: duplicate shortcuts can be created
Problem: When [gui/shortcuts]fulldefaults is true, then you can't clear the shortcut of a command which is only in the full set.
In bool KeyConfigPrefs::Commit(), for a command only in the full set, dkey = "", so if the key is set to "", there will be no entry in NewKeys for this.

In CommandListEntry *CommandManager::NewIdentifier, if [gui/shortcuts]fulldefaults is true, then for all commands the key is initially set to the default. For the shortcut that should have been cleared, there is no entry in NewKeys to override this, so it is in fact not cleared.

Fix: In bool KeyConfigPrefs::Commit(), when [gui/shortcuts]fulldefaults is true, use the full set of defaults to find the default key.
2018-02-22 18:36:39 +00:00

724 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, wxWindowID winid,
const wxString &name)
/* i18n-hint: as in computer keyboard (not musical!) */
: PrefsPanel(parent, winid, _("Keyboard")),
mView(NULL),
mKey(NULL),
mFilter(NULL),
mFilterTimer(this, FilterTimerID),
mFilterPending(false)
{
Populate();
if (!name.empty()) {
auto index = mView->GetIndexByName(name);
mView->SelectNode(index);
}
}
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->Bind(wxEVT_KEY_DOWN,
&KeyConfigPrefs::OnFilterKeyDown,
this);
mFilter->Bind(wxEVT_CHAR,
&KeyConfigPrefs::OnFilterChar,
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->Bind(wxEVT_KEY_DOWN,
&KeyConfigPrefs::OnHotkeyKeyDown,
this);
mKey->Bind(wxEVT_CHAR,
&KeyConfigPrefs::OnHotkeyChar,
this);
mKey->Bind(wxEVT_KILL_FOCUS,
&KeyConfigPrefs::OnHotkeyKillFocus,
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" );
std::transform( MaxListOnly.begin(), MaxListOnly.end(), MaxListOnly.begin(),
KeyStringNormalize );
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);
bool bFull = gPrefs->ReadBool(wxT("/GUI/Shortcuts/FullDefaults"), false);
for (size_t i = 0; i < mNames.GetCount(); i++) {
wxString dkey = bFull ? KeyStringNormalize(mDefaultKeys[i]) : 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::operator () (wxWindow *parent, wxWindowID winid)
{
wxASSERT(parent); // to justify safenew
auto result = safenew KeyConfigPrefs{ parent, winid, mName };
return result;
}