/* * InputHandler.cpp * ---------------- * Purpose: Implementation of keyboard input handling, keymap loading, ... * Notes : (currently none) * Authors: OpenMPT Devs * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. */ #include "stdafx.h" #include "CommandSet.h" #include "InputHandler.h" #include "resource.h" #include "Mainfrm.h" #include "../soundlib/MIDIEvents.h" OPENMPT_NAMESPACE_BEGIN #define TRANSITIONBIT 0x8000 #define REPEATBIT 0x4000 CInputHandler::CInputHandler(CWnd *mainframe) { m_pMainFrm = mainframe; //Init CommandSet and Load defaults m_activeCommandSet = std::make_unique(); m_lastCommands.fill(kcNull); mpt::PathString sDefaultPath = theApp.GetConfigPath() + P_("Keybindings.mkb"); const bool bNoExistingKbdFileSetting = TrackerSettings::Instance().m_szKbdFile.empty(); // 1. Try to load keybindings from the path saved in the settings. // 2. If the setting doesn't exist or the loading fails, try to load from default location. // 3. If neither one of these worked, load default keybindings from resources. // 4. If there were no keybinding setting already, create a keybinding file to default location // and set its path to settings. if (bNoExistingKbdFileSetting || !(m_activeCommandSet->LoadFile(TrackerSettings::Instance().m_szKbdFile))) { if (bNoExistingKbdFileSetting) TrackerSettings::Instance().m_szKbdFile = sDefaultPath; bool bSuccess = false; if (sDefaultPath.IsFile()) bSuccess = m_activeCommandSet->LoadFile(sDefaultPath); if (!bSuccess) { // Load keybindings from resources. MPT_LOG_GLOBAL(LogDebug, "InputHandler", U_("Loading keybindings from resources\n")); bSuccess = m_activeCommandSet->LoadDefaultKeymap(); if (bSuccess && bNoExistingKbdFileSetting) { m_activeCommandSet->SaveFile(TrackerSettings::Instance().m_szKbdFile); } } if (!bSuccess) ErrorBox(IDS_UNABLE_TO_LOAD_KEYBINDINGS); } // We will only overwrite the default Keybindings.mkb file from now on. TrackerSettings::Instance().m_szKbdFile = sDefaultPath; //Get Keymap m_activeCommandSet->GenKeyMap(m_keyMap); SetupSpecialKeyInterception(); // Feature: use Windows keys as modifier keys, intercept special keys } CommandID CInputHandler::SendCommands(CWnd *wnd, const KeyMapRange &cmd) { CommandID executeCommand = kcNull; if(wnd != nullptr) { // Some commands (e.g. open/close/document switching) may invalidate the key map and thus its iterators. // To avoid this problem, copy over the elements we are interested in before sending commands. std::vector commands; commands.reserve(std::distance(cmd.first, cmd.second)); for(auto i = cmd.first; i != cmd.second; i++) { commands.push_back(*i); } for(const auto &i : commands) { m_lastCommands[m_lastCommandPos] = i.second; m_lastCommandPos = (m_lastCommandPos + 1) % m_lastCommands.size(); if(wnd->SendMessage(WM_MOD_KEYCOMMAND, i.second, i.first.AsLPARAM()) != kcNull) { // Command was handled, no need to let the OS handle the key executeCommand = i.second; } } } return executeCommand; } CommandID CInputHandler::GeneralKeyEvent(InputTargetContext context, int code, WPARAM wParam, LPARAM lParam) { KeyMapRange cmd = { m_keyMap.end(), m_keyMap.end() }; KeyEventType keyEventType; if(code == HC_ACTION) { //Get the KeyEventType (key up, key down, key repeat) DWORD scancode = static_cast(lParam) >> 16; if((scancode & 0xC000) == 0xC000) { keyEventType = kKeyEventUp; } else if((scancode & 0xC000) == 0x0000) { keyEventType = kKeyEventDown; } else { keyEventType = kKeyEventRepeat; } // Catch modifier change (ctrl, alt, shift) - Only check on keyDown or keyUp. // NB: we want to catch modifiers even when the input handler is locked if(keyEventType == kKeyEventUp || keyEventType == kKeyEventDown) { scancode = (static_cast(lParam) >> 16) & 0x1FF; CatchModifierChange(wParam, keyEventType, scancode); } if(!InterceptSpecialKeys(static_cast(wParam), static_cast(lParam), true) && !IsBypassed()) { // only execute command when the input handler is not locked // and the input is not a consequence of special key interception. cmd = m_keyMap.equal_range(KeyCombination(context, m_modifierMask, static_cast(wParam), keyEventType)); } } if(code == HC_MIDI) { cmd = m_keyMap.equal_range(KeyCombination(context, ModMidi, static_cast(wParam), static_cast(lParam))); } return SendCommands(m_pMainFrm, cmd); } CommandID CInputHandler::KeyEvent(InputTargetContext context, UINT &nChar, UINT &/*nRepCnt*/, UINT &nFlags, KeyEventType keyEventType, CWnd *pSourceWnd) { if(InterceptSpecialKeys(nChar, nFlags, false)) return kcDummyShortcut; KeyMapRange cmd = m_keyMap.equal_range(KeyCombination(context, m_modifierMask, nChar, keyEventType)); if(pSourceWnd == nullptr) pSourceWnd = m_pMainFrm; // By default, send command message to main frame. return SendCommands(pSourceWnd, cmd); } // Feature: use Windows keys as modifier keys, intercept special keys bool CInputHandler::InterceptSpecialKeys(UINT nChar, UINT nFlags, bool generateMsg) { KeyEventType keyEventType = GetKeyEventType(HIWORD(nFlags)); enum { VK_NonExistentKey = VK_F24+1 }; if(nChar == VK_NonExistentKey) { return true; } else if(m_bInterceptWindowsKeys && (nChar == VK_LWIN || nChar == VK_RWIN)) { if(keyEventType == kKeyEventDown) { INPUT inp[2]; inp[0].type = inp[1].type = INPUT_KEYBOARD; inp[0].ki.time = inp[1].ki.time = 0; inp[0].ki.dwExtraInfo = inp[1].ki.dwExtraInfo = 0; inp[0].ki.wVk = inp[1].ki.wVk = VK_NonExistentKey; inp[0].ki.wScan = inp[1].ki.wScan = 0; inp[0].ki.dwFlags = 0; inp[1].ki.dwFlags = KEYEVENTF_KEYUP; SendInput(2, inp, sizeof(INPUT)); } } if((nChar == VK_NUMLOCK && m_bInterceptNumLock) || (nChar == VK_CAPITAL && m_bInterceptCapsLock) || (nChar == VK_SCROLL && m_bInterceptScrollLock)) { if(GetMessageExtraInfo() == 0xC0FFEE) { SetMessageExtraInfo(0); return true; } else if(keyEventType == kKeyEventDown && generateMsg) { // Prevent keys from lighting up by simulating a second press. INPUT inp[2]; inp[0].type = inp[1].type = INPUT_KEYBOARD; inp[0].ki.time = inp[1].ki.time = 0; inp[0].ki.dwExtraInfo = inp[1].ki.dwExtraInfo = 0xC0FFEE; inp[0].ki.wVk = inp[1].ki.wVk = static_cast(nChar); inp[0].ki.wScan = inp[1].ki.wScan = 0; inp[0].ki.dwFlags = KEYEVENTF_KEYUP; inp[1].ki.dwFlags = 0; SendInput(2, inp, sizeof(INPUT)); } } return false; }; void CInputHandler::SetupSpecialKeyInterception() { m_bInterceptWindowsKeys = m_bInterceptNumLock = m_bInterceptCapsLock = m_bInterceptScrollLock = false; for(const auto &i : m_keyMap) { ASSERT(i.second != kcNull); if(i.first.Modifier() == ModWin) m_bInterceptWindowsKeys = true; if(i.first.KeyCode() == VK_NUMLOCK) m_bInterceptNumLock = true; if(i.first.KeyCode() == VK_CAPITAL) m_bInterceptCapsLock = true; if(i.first.KeyCode() == VK_SCROLL) m_bInterceptScrollLock = true; } }; //Deal with Modifier keypresses. Private surouting used above. bool CInputHandler::CatchModifierChange(WPARAM wParam, KeyEventType keyEventType, int scancode) { FlagSet modifierMask = ModNone; // Scancode for right modifier keys should have bit 8 set, but Right Shift is actually 0x36. const bool isRight = ((scancode & 0x100) || scancode == 0x36) && TrackerSettings::Instance().MiscDistinguishModifiers; switch(wParam) { case VK_CONTROL: modifierMask.set(isRight ? ModRCtrl : ModCtrl); break; case VK_SHIFT: modifierMask.set(isRight ? ModRShift : ModShift); break; case VK_MENU: modifierMask.set(isRight ? ModRAlt : ModAlt); break; case VK_LWIN: case VK_RWIN: // Feature: use Windows keys as modifier keys modifierMask.set(ModWin); break; } if (modifierMask) // This keypress just changed the modifier mask { if (keyEventType == kKeyEventDown) { m_modifierMask.set(modifierMask); // Right Alt is registered as Ctrl+Alt. // Left Ctrl + Right Alt seems like a pretty difficult to use key combination anyway, so just ignore Ctrl. if(scancode == 0x138) m_modifierMask.reset(ModCtrl); #ifdef _DEBUG LogModifiers(); #endif } else if (keyEventType == kKeyEventUp) m_modifierMask.reset(modifierMask); return true; } return false; } // Translate MIDI messages to shortcut commands CommandID CInputHandler::HandleMIDIMessage(InputTargetContext context, uint32 message) { auto byte1 = MIDIEvents::GetDataByte1FromEvent(message), byte2 = MIDIEvents::GetDataByte2FromEvent(message); switch(MIDIEvents::GetTypeFromEvent(message)) { case MIDIEvents::evControllerChange: if(byte2 != 0) { // Only capture MIDI CCs for now. Some controllers constantly send some MIDI CCs with value 0 // (e.g. the Roland D-50 sends CC123 whenenver all notes have been released), so we will ignore those. return GeneralKeyEvent(context, HC_MIDI, byte1, kKeyEventDown); } break; case MIDIEvents::evNoteOff: byte2 = 0; [[fallthrough]]; case MIDIEvents::evNoteOn: if(byte2 != 0) { return GeneralKeyEvent(context, HC_MIDI, byte1 | 0x80, kKeyEventDown); } else { // If the key-down triggered a note, we still want that note to be stopped. So we always pretend that no key was assigned to this event GeneralKeyEvent(context, HC_MIDI, byte1 | 0x80, kKeyEventUp); } break; } return kcNull; } int CInputHandler::GetKeyListSize(CommandID cmd) const { return m_activeCommandSet->GetKeyListSize(cmd); } //----------------------- Misc void CInputHandler::LogModifiers() { MPT_LOG_GLOBAL(LogDebug, "InputHandler", U_("----------------------------------\n")); MPT_LOG_GLOBAL(LogDebug, "InputHandler", m_modifierMask[ModCtrl] ? U_("Ctrl On") : U_("Ctrl --")); MPT_LOG_GLOBAL(LogDebug, "InputHandler", m_modifierMask[ModShift] ? U_("\tShft On") : U_("\tShft --")); MPT_LOG_GLOBAL(LogDebug, "InputHandler", m_modifierMask[ModAlt] ? U_("\tAlt On") : U_("\tAlt --")); MPT_LOG_GLOBAL(LogDebug, "InputHandler", m_modifierMask[ModWin] ? U_("\tWin On\n") : U_("\tWin --\n")); } KeyEventType CInputHandler::GetKeyEventType(UINT nFlags) { if (nFlags & TRANSITIONBIT) { // Key released return kKeyEventUp; } else if (nFlags & REPEATBIT) { // Key repeated return kKeyEventRepeat; } else { // New key down return kKeyEventDown; } } bool CInputHandler::SelectionPressed() const { int nSelectionKeys = m_activeCommandSet->GetKeyListSize(kcSelect); KeyCombination key; for (int k=0; kGetKey(kcSelect, k); if (m_modifierMask & key.Modifier()) { return true; } } return false; } bool CInputHandler::ShiftPressed() const { return m_modifierMask[ModShift | ModRShift]; } bool CInputHandler::CtrlPressed() const { return m_modifierMask[ModCtrl | ModRCtrl]; } bool CInputHandler::AltPressed() const { return m_modifierMask[ModAlt | ModRAlt]; } void CInputHandler::Bypass(bool b) { if(b) m_bypassCount++; else m_bypassCount--; ASSERT(m_bypassCount >= 0); } bool CInputHandler::IsBypassed() const { return m_bypassCount > 0; } FlagSet CInputHandler::GetModifierMask() const { return m_modifierMask; } void CInputHandler::SetModifierMask(FlagSet mask) { m_modifierMask = mask; } CString CInputHandler::GetKeyTextFromCommand(CommandID c, const TCHAR *prependText) const { CString s; if(prependText != nullptr) { s = prependText; s.AppendChar(_T('\t')); } s += m_activeCommandSet->GetKeyTextFromCommand(c, 0); return s; } CString CInputHandler::GetMenuText(UINT id) const { static constexpr std::tuple MenuItems[] = { { ID_FILE_NEW, kcFileNew, _T("&New") }, { ID_FILE_OPEN, kcFileOpen, _T("&Open...") }, { ID_FILE_OPENTEMPLATE, kcNull, _T("Open &Template") }, { ID_FILE_CLOSE, kcFileClose, _T("&Close") }, { ID_FILE_CLOSEALL, kcFileCloseAll, _T("C&lose All") }, { ID_FILE_APPENDMODULE, kcFileAppend, _T("Appen&d Module...") }, { ID_FILE_SAVE, kcFileSave, _T("&Save") }, { ID_FILE_SAVE_AS, kcFileSaveAs, _T("Save &As...") }, { ID_FILE_SAVE_COPY, kcFileSaveCopy, _T("Save Cop&y...") }, { ID_FILE_SAVEASTEMPLATE, kcFileSaveTemplate, _T("Sa&ve as Template") }, { ID_FILE_SAVEASWAVE, kcFileSaveAsWave, _T("Stream Export (&WAV, FLAC, MP3, etc.)...") }, { ID_FILE_SAVEMIDI, kcFileSaveMidi, _T("Export as M&IDI...") }, { ID_FILE_SAVEOPL, kcFileSaveOPL, _T("Export O&PL Register Dump...") }, { ID_FILE_SAVECOMPAT, kcFileExportCompat, _T("Compatibility &Export...") }, { ID_IMPORT_MIDILIB, kcFileImportMidiLib, _T("Import &MIDI Library...") }, { ID_ADD_SOUNDBANK, kcFileAddSoundBank, _T("Add Sound &Bank...") }, { ID_PLAYER_PLAY, kcPlayPauseSong, _T("Pause / &Resume") }, { ID_PLAYER_PLAYFROMSTART, kcPlaySongFromStart, _T("&Play from Start") }, { ID_PLAYER_STOP, kcStopSong, _T("&Stop") }, { ID_PLAYER_PAUSE, kcPauseSong, _T("P&ause") }, { ID_MIDI_RECORD, kcMidiRecord, _T("&MIDI Record") }, { ID_ESTIMATESONGLENGTH, kcEstimateSongLength, _T("&Estimate Song Length") }, { ID_APPROX_BPM, kcApproxRealBPM, _T("Approximate Real &BPM") }, { ID_EDIT_UNDO, kcEditUndo, _T("&Undo") }, { ID_EDIT_REDO, kcEditRedo, _T("&Redo") }, { ID_EDIT_CUT, kcEditCut, _T("Cu&t") }, { ID_EDIT_COPY, kcEditCopy, _T("&Copy") }, { ID_EDIT_PASTE, kcEditPaste, _T("&Paste") }, { ID_EDIT_SELECT_ALL, kcEditSelectAll, _T("Select &All") }, { ID_EDIT_CLEANUP, kcNull, _T("C&leanup") }, { ID_EDIT_FIND, kcEditFind, _T("&Find / Replace") }, { ID_EDIT_FINDNEXT, kcEditFindNext, _T("Find &Next") }, { ID_EDIT_GOTO_MENU, kcPatternGoto, _T("&Goto") }, { ID_EDIT_SPLITKEYBOARDSETTINGS, kcShowSplitKeyboardSettings, _T("Split &Keyboard Settings") }, // "Paste Special" sub menu { ID_EDIT_PASTE_SPECIAL, kcEditMixPaste, _T("&Mix Paste") }, { ID_EDIT_MIXPASTE_ITSTYLE, kcEditMixPasteITStyle, _T("M&ix Paste (IT Style)") }, { ID_EDIT_PASTEFLOOD, kcEditPasteFlood, _T("Paste Fl&ood") }, { ID_EDIT_PUSHFORWARDPASTE, kcEditPushForwardPaste, _T("&Push Forward Paste (Insert)") }, { ID_VIEW_GLOBALS, kcViewGeneral, _T("&General") }, { ID_VIEW_SAMPLES, kcViewSamples, _T("&Samples") }, { ID_VIEW_PATTERNS, kcViewPattern, _T("&Patterns") }, { ID_VIEW_INSTRUMENTS, kcViewInstruments, _T("&Instruments") }, { ID_VIEW_COMMENTS, kcViewComments, _T("&Comments") }, { ID_VIEW_OPTIONS, kcViewOptions, _T("S&etup") }, { ID_VIEW_TOOLBAR, kcViewMain, _T("&Main") }, { IDD_TREEVIEW, kcViewTree, _T("&Tree") }, { ID_PLUGIN_SETUP, kcViewAddPlugin, _T("Pl&ugin Manager") }, { ID_CHANNEL_MANAGER, kcViewChannelManager, _T("Ch&annel Manager") }, { ID_CLIPBOARD_MANAGER, kcToggleClipboardManager, _T("C&lipboard Manager") }, { ID_VIEW_SONGPROPERTIES, kcViewSongProperties, _T("Song P&roperties") }, { ID_PATTERN_MIDIMACRO, kcShowMacroConfig, _T("&Zxx Macro Configuration") }, { ID_VIEW_MIDIMAPPING, kcViewMIDImapping, _T("&MIDI Mapping") }, { ID_VIEW_EDITHISTORY, kcViewEditHistory, _T("Edit &History") }, // Help submenu { ID_HELPSHOW, kcHelp, _T("&Help") }, { ID_EXAMPLE_MODULES, kcNull, _T("&Example Modules") }, }; for(const auto & [cmdID, command, text] : MenuItems) { if(id == cmdID) { if(command != kcNull) return GetKeyTextFromCommand(command, text); else return text; } } MPT_ASSERT_NOTREACHED(); return _T("Unknown Item"); } void CInputHandler::UpdateMainMenu() { CMenu *pMenu = (CMainFrame::GetMainFrame())->GetMenu(); if (!pMenu) return; pMenu->GetSubMenu(0)->ModifyMenu(0, MF_BYPOSITION | MF_STRING, 0, GetMenuText(ID_FILE_NEW)); static constexpr int MenuItems[] = { ID_FILE_OPEN, ID_FILE_APPENDMODULE, ID_FILE_CLOSE, ID_FILE_SAVE, ID_FILE_SAVE_AS, ID_FILE_SAVEASWAVE, ID_FILE_SAVEMIDI, ID_FILE_SAVECOMPAT, ID_IMPORT_MIDILIB, ID_ADD_SOUNDBANK, ID_PLAYER_PLAY, ID_PLAYER_PLAYFROMSTART, ID_PLAYER_STOP, ID_PLAYER_PAUSE, ID_MIDI_RECORD, ID_ESTIMATESONGLENGTH, ID_APPROX_BPM, ID_EDIT_UNDO, ID_EDIT_REDO, ID_EDIT_CUT, ID_EDIT_COPY, ID_EDIT_PASTE, ID_EDIT_PASTE_SPECIAL, ID_EDIT_MIXPASTE_ITSTYLE, ID_EDIT_PASTEFLOOD, ID_EDIT_PUSHFORWARDPASTE, ID_EDIT_SELECT_ALL, ID_EDIT_FIND, ID_EDIT_FINDNEXT, ID_EDIT_GOTO_MENU, ID_EDIT_SPLITKEYBOARDSETTINGS, ID_VIEW_GLOBALS, ID_VIEW_SAMPLES, ID_VIEW_PATTERNS, ID_VIEW_INSTRUMENTS, ID_VIEW_COMMENTS, ID_VIEW_TOOLBAR, IDD_TREEVIEW, ID_VIEW_OPTIONS, ID_PLUGIN_SETUP, ID_CHANNEL_MANAGER, ID_CLIPBOARD_MANAGER, ID_VIEW_SONGPROPERTIES, ID_VIEW_SONGPROPERTIES, ID_PATTERN_MIDIMACRO, ID_VIEW_EDITHISTORY, ID_HELPSHOW, }; for(const auto id : MenuItems) { pMenu->ModifyMenu(id, MF_BYCOMMAND | MF_STRING, id, GetMenuText(id)); } } void CInputHandler::SetNewCommandSet(const CCommandSet *newSet) { m_activeCommandSet->Copy(newSet); m_activeCommandSet->GenKeyMap(m_keyMap); SetupSpecialKeyInterception(); // Feature: use Windows keys as modifier keys, intercept special keys UpdateMainMenu(); } bool CInputHandler::SetEffectLetters(const CModSpecifications &modSpecs) { MPT_LOG_GLOBAL(LogDebug, "InputHandler", U_("Changing command set.")); bool retval = m_activeCommandSet->QuickChange_SetEffects(modSpecs); if(retval) m_activeCommandSet->GenKeyMap(m_keyMap); return retval; } bool CInputHandler::IsKeyPressHandledByTextBox(DWORD key, HWND hWnd) const { if(hWnd == nullptr) return false; TCHAR activeWindowClassName[6]; GetClassName(hWnd, activeWindowClassName, mpt::saturate_cast(std::size(activeWindowClassName))); const bool textboxHasFocus = _tcsicmp(activeWindowClassName, _T("Edit")) == 0; if(!textboxHasFocus) return false; //Alpha-numerics (only shift or no modifier): if(!GetModifierMask().test_any_except(ModShift) && ((key>='A'&&key<='Z') || (key>='0'&&key<='9') || key==VK_DIVIDE || key==VK_MULTIPLY || key==VK_SPACE || key==VK_RETURN || key==VK_CAPITAL || (key>=VK_OEM_1 && key<=VK_OEM_3) || (key>=VK_OEM_4 && key<=VK_OEM_8))) return true; //navigation (any modifier): if(key == VK_LEFT || key == VK_RIGHT || key == VK_UP || key == VK_DOWN || key == VK_HOME || key == VK_END || key == VK_DELETE || key == VK_INSERT || key == VK_BACK) return true; //Copy paste etc.. if(GetModifierMask() == ModCtrl && (key == 'Y' || key == 'Z' || key == 'X' || key == 'C' || key == 'V' || key == 'A')) return true; return false; } BypassInputHandler::BypassInputHandler() { if(CMainFrame::GetInputHandler()) { bypassed = true; CMainFrame::GetInputHandler()->Bypass(true); } } BypassInputHandler::~BypassInputHandler() { if(bypassed) { CMainFrame::GetInputHandler()->Bypass(false); bypassed = false; } } OPENMPT_NAMESPACE_END