winamp/Src/external_dependencies/openmpt-trunk/mptrack/InputHandler.cpp
2024-09-24 14:54:57 +02:00

633 lines
19 KiB
C++

/*
* 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<CCommandSet>();
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<KeyMap::value_type> 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<LONG>(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<LONG>(lParam) >> 16) & 0x1FF;
CatchModifierChange(wParam, keyEventType, scancode);
}
if(!InterceptSpecialKeys(static_cast<UINT>(wParam), static_cast<LONG>(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<UINT>(wParam), keyEventType));
}
}
if(code == HC_MIDI)
{
cmd = m_keyMap.equal_range(KeyCombination(context, ModMidi, static_cast<UINT>(wParam), static_cast<KeyEventType>(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<WORD>(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<Modifiers> 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; k<nSelectionKeys; k++)
{
key = m_activeCommandSet->GetKey(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<Modifiers> CInputHandler::GetModifierMask() const
{
return m_modifierMask;
}
void CInputHandler::SetModifierMask(FlagSet<Modifiers> 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<UINT, CommandID, const TCHAR *> 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<int>(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