// Plugin Template // #include "stdafx.h" #include "PluginInterface.h" #include "AboutDialog.h" #include "MenuManager.h" #include "Scintilla.h" #include "WcharMbcsConverter.h" #include "PythonHandler.h" #include "PythonConsole.h" #include "ShortcutDlg.h" #include "Notepad_plus_msgs.h" #include "HelpController.h" #include "PythonScript/NppPythonScript.h" #include "ReplacementContainer.h" #include "GILManager.h" #include "MainThread.h" #include "ScintillaCallbackCounter.h" #include "MutexHolder.h" #define CHECK_INITIALISED() if (!g_initialised) initialisePython() /* Info for Notepad++ */ static FuncItem *funcItem = NULL; /* Global data */ static NppData nppData; static HANDLE g_hModule = NULL; /* Dialogs */ static AboutDialog aboutDlg; static ShortcutDlg *g_shortcutDlg = NULL; static boost::shared_ptr g_console(NULL); static bool g_bToggleConsoleFlag = false; // Paths static char g_pluginDir[MAX_PATH]; static char g_configDir[MAX_PATH]; static TCHAR g_tPluginDir[MAX_PATH]; static TCHAR g_tConfigDir[MAX_PATH]; static tstring g_previousScript; static bool g_infoSet = false; static bool g_initialised = false; static bool previousScript_clicked = false; // Scripts on the menu static std::vector g_menuScripts; // Scripts on the toolbar static std::vector< std::pair* > g_toolbarScripts; static void doAbout(); static void newScript(); static void toggleConsole(); static void showShortcutDlg(); static void stopScript(); static void runScript(idx_t number); static void runScript(const TCHAR *script, bool synchronous, HANDLE completedEvent = NULL, bool allowQueuing = false); static void runStatement(const TCHAR *statement, bool synchronous, HANDLE completedEvent = NULL, bool allowQueuing = false); static void shutdown(void *); static void doHelp(); static void previousScript(); static FuncItem* getGeneratedFuncItemArray(int *nbF); // Run script functions // Main python handler/wrapper static NppPythonScript::PythonHandler *pythonHandler; BOOL APIENTRY DllMain( HMODULE hModule, DWORD ul_reason_for_call, LPVOID /* lpReserved */ ) { g_hModule = hModule; switch (ul_reason_for_call) { case DLL_PROCESS_ATTACH: break; case DLL_THREAD_ATTACH: break; case DLL_THREAD_DETACH: break; case DLL_PROCESS_DETACH: break; NO_DEFAULT_CASE; } return TRUE; } extern "C" __declspec(dllexport) void setInfo(NppData notepadPlusData) { nppData = notepadPlusData; #ifdef DEBUG_STARTUP MessageBox(NULL, _T("setInfo"), PLUGIN_NAME, 0); #endif // Get the two key directories (plugins config and the Npp dir) ::SendMessage(nppData._nppHandle, NPPM_GETPLUGINSCONFIGDIR, MAX_PATH, reinterpret_cast(g_tConfigDir)); strcpy_s(g_configDir, MAX_PATH, WcharMbcsConverter::tchar2char(g_tConfigDir).get()); ::SendMessage(nppData._nppHandle, NPPM_GETNPPDIRECTORY, MAX_PATH, reinterpret_cast(g_tPluginDir)); _tcscat_s(g_tPluginDir, MAX_PATH, _T("\\plugins")); strcpy_s(g_pluginDir, MAX_PATH, WcharMbcsConverter::tchar2char(g_tPluginDir).get()); ConfigFile::create(g_tConfigDir, g_tPluginDir, reinterpret_cast(g_hModule)); MenuManager::create(nppData._nppHandle, reinterpret_cast(g_hModule), runScript); g_infoSet = true; } extern "C" __declspec(dllexport) CONST TCHAR * getName() { return PLUGIN_NAME; } extern "C" __declspec(dllexport) FuncItem * getFuncsArray(int *nbF) { if (g_infoSet) { #ifdef DEBUG_STARTUP MessageBox(NULL, _T("Python GetFuncsArray"), PLUGIN_NAME, 0); #endif funcItem = getGeneratedFuncItemArray(nbF); } else { MessageBox(NULL, _T("A fatal error has occurred. Notepad++ has incorrectly called getFuncsArray() before setInfo(). No menu items will be available for PythonScript."), PLUGIN_NAME, 0); funcItem = (FuncItem*) malloc(sizeof(FuncItem)); if(funcItem) { memset(funcItem, 0, sizeof(FuncItem)); _tcscpy_s(funcItem[0]._itemName, 64, _T("About - Python Script Disabled")); funcItem[0]._pFunc = doAbout; } *nbF = 1; } return funcItem; } static FuncItem* getGeneratedFuncItemArray(int *nbF) { MenuManager* menuManager = MenuManager::getInstance(); MenuManager::ItemVectorTD items; items.reserve(8); idx_t stopScriptIndex; idx_t dynamicStartIndex; idx_t scriptsMenuIndex; idx_t runPreviousIndex; items.push_back(std::pair(_T("New Script"), newScript)); items.push_back(std::pair(_T("Show Console"), toggleConsole)); items.push_back(std::pair(_T("--"), reinterpret_cast(NULL))); items.push_back(std::pair(_T("Stop Script"), stopScript)); stopScriptIndex = items.size() - 1; items.push_back(std::pair(_T("--"), reinterpret_cast(NULL))); items.push_back(std::pair(_T("Run Previous Script"), previousScript)); runPreviousIndex = items.size() - 1; items.push_back(std::pair(_T("--"), reinterpret_cast(NULL))); scriptsMenuIndex = items.size() - 1; items.push_back(std::pair(_T("Configuration..."), showShortcutDlg)); // Add dynamic scripts above the Configuration option - an extra separator will automatically // be added to the end of the list, if there are items in the dynamic menu dynamicStartIndex = items.size() - 1; items.push_back(std::pair(_T("--"), reinterpret_cast(NULL))); items.push_back(std::pair(_T("Context-Help"), doHelp)); items.push_back(std::pair(_T("About"), doAbout)); FuncItem* funcItems = menuManager->getFuncItemArray(nbF, items, runScript, dynamicStartIndex, scriptsMenuIndex, stopScriptIndex, runPreviousIndex); return funcItems; } static void initialise() { g_mainThreadID = ::GetCurrentThreadId(); g_console.reset(new NppPythonScript::PythonConsole(nppData._nppHandle)); pythonHandler = new NppPythonScript::PythonHandler(g_tPluginDir, g_tConfigDir, (HINSTANCE)g_hModule, nppData._nppHandle, nppData._scintillaMainHandle, nppData._scintillaSecondHandle, g_console); aboutDlg.initDialog((HINSTANCE)g_hModule, nppData); g_shortcutDlg = new ShortcutDlg((HINSTANCE)g_hModule, nppData, _T("\\PythonScript\\scripts")); g_console->init((HINSTANCE)g_hModule, nppData); MenuManager* menuManager = MenuManager::getInstance(); //menuManager->idsInitialised(); menuManager->populateScriptsMenu(); menuManager->stopScriptEnabled(false); menuManager->initPreviousScript(); } static void initialisePython() { g_initialised = true; DWORD startTicks = GetTickCount(); DEBUG_TRACE_S(("starting python at %ld", startTicks)); pythonHandler->initPython(); g_console->initPython(pythonHandler); pythonHandler->runStartupScripts(); DWORD endTicks = GetTickCount(); g_console->message("Python "); g_console->message(Py_GetVersion()); char result[200]; sprintf_s(result, 200, "\nInitialisation took %ldms\nReady.\n", endTicks-startTicks); g_console->message(result); } static void registerToolbarIcons() { #ifdef DEBUG_STARTUP MessageBox(NULL, _T("Register toolbar icons"), PLUGIN_NAME, 0); #endif MenuManager* menuManager = MenuManager::getInstance(); menuManager->idsInitialised(); menuManager->configureToolbarIcons(); } extern "C" __declspec(dllexport) void beNotified(SCNotification *notifyCode) { /* This switch is split into two * 1. Notifications that must be run BEFORE any registered Python callbacks, and * 2. Notifications that must be run AFTER any registered Python callbacks */ // Note: The callback isn't actually complete until after this method returns, and // scintilla has finished through it's iterator of watchers. However, under "normal" circumstances, this stops a synchronous // callback from calling a function that changes the watchers on the internal Scintilla document. // Async callbacks aren't an issue, as they may call into scintilla whilst a callback is ongoing, but as scintilla is running // on a single thread, it waits until the callback code is complete for continuing (ie. processing the next message in the message loop). NppPythonScript::DepthLevel callbackCount; if (notifyCode->nmhdr.code >= SCN_FIRST_NOTIFICATION) { callbackCount = NppPythonScript::ScintillaCallbackCounter::inCallback(); } switch(notifyCode->nmhdr.code) { case NPPN_READY: { #ifdef DEBUG_STARTUP MessageBox(NULL, _T("NPPN_READY"), PLUGIN_NAME, 0); #endif initialise(); if (g_bToggleConsoleFlag) toggleConsole(); // fix possible missing PS console (Notepad++ DockingManager-init calls the PS toggleConsole() published func before the PS init...) ConfigFile *config = ConfigFile::getInstance(); if (config->getSetting(_T("STARTUP")) == _T("ATSTARTUP")) { CHECK_INITIALISED(); } } break; case NPPN_FILESAVED: { TCHAR filename[MAX_PATH]{}; ::SendMessage(nppData._nppHandle, NPPM_GETFULLPATHFROMBUFFERID, notifyCode->nmhdr.idFrom, reinterpret_cast(filename)); ConfigFile *configFile = ConfigFile::getInstance(); const tstring machineScripts = configFile->getMachineScriptsDir().c_str(); const tstring userScripts = configFile->getUserScriptsDir().c_str(); if (_tcsnicmp(filename, machineScripts.c_str(), machineScripts.size()) == 0 || _tcsnicmp(filename, userScripts.c_str(), userScripts.size()) == 0) { MenuManager::getInstance()->refreshScriptsMenu(); } } break; case NPPN_TBMODIFICATION: registerToolbarIcons(); break; case NPPN_SHORTCUTREMAPPED: { MenuManager *menuManager = MenuManager::getInstance(); menuManager->updateShortcut(notifyCode->nmhdr.idFrom, reinterpret_cast(notifyCode->nmhdr.hwndFrom)); } break; default: // Other messages can just be ignored break; } // Notify the scripts if (pythonHandler) pythonHandler->notify(notifyCode); // Post notifications handlers switch(notifyCode->nmhdr.code) { case NPPN_SHUTDOWN: { DWORD shutdownThreadID; CreateThread(NULL, NULL, (LPTHREAD_START_ROUTINE)shutdown, NULL, NULL, &shutdownThreadID); } break; default: // Ignore all other messages break; } } extern "C" __declspec(dllexport) LRESULT messageProc(UINT message, WPARAM wParam, LPARAM lParam) { switch(message) { case NPPM_MSGTOPLUGIN: { CommunicationInfo* ci = reinterpret_cast(lParam); switch(ci->internalMsg) { case PYSCR_EXECSCRIPT: case PYSCR_EXECSTATEMENT: { CHECK_INITIALISED(); PythonScript_Exec* pse = reinterpret_cast(ci->info); if (pse->structVersion != 1) { return TRUE; } std::shared_ptr script = WcharMbcsConverter::tchar2tchar(pse->script); bool synchronous = (pse->flags & PYSCRF_SYNC) == PYSCRF_SYNC; if (PYSCR_EXECSCRIPT == ci->internalMsg) { runScript(script.get(), synchronous, pse->completedEvent, true); } else { runStatement(script.get(), synchronous, pse->completedEvent, true); } pse->deliverySuccess = TRUE; return FALSE; } case PYSCR_SHOWCONSOLE: if (g_console) { g_console->showDialog(); } return FALSE; case PYSCR_RUNREPLACE: { NppPythonScript::ReplacementContainer *replacementContainer = reinterpret_cast(ci->info); replacementContainer->runReplacements(); return FALSE; } default: // Other messages are unknown, hence ignore break; } } break; case WM_COMMAND: MenuManager::getInstance()->processWmCommand(wParam, lParam); break; default: // Other messages can just be ignored break; } return TRUE; } #ifdef _UNICODE extern "C" __declspec(dllexport) BOOL isUnicode() { return TRUE; } #endif static void doAbout() { aboutDlg.doDialog(); } static void stopScript() { if (pythonHandler) { pythonHandler->stopScript(); } } static bool shortcutKeyHasCtrl(idx_t number) { bool retVal = false; idx_t cmdID = MenuManager::getInstance()->getOriginalCommandID(number); if (cmdID) { ShortcutKey key; BOOL hasShortcut = ::SendMessage(nppData._nppHandle, NPPM_GETSHORTCUTBYCMDID, cmdID, reinterpret_cast(&key)); if (hasShortcut && 0 != key._key && key._isCtrl && !key._isAlt && !key._isShift) { retVal = true; } } return retVal; } static void runScript(idx_t number) { /* If the shortcut for the given script number does not have a control in it, * (or no shortcut key is assigned), then we can pretend the user clicked the menu option. * runScript() will then check if Ctrl is held down, if it is, it will edit the script. * Obviously if this menu item's key DOES have Ctrl in it, (and we're on N++ 5.8 or upwards) * then we can't tell the difference (as we haven't subclassed N++). * If that's the case, then ctrl-click won't work. That's just tough, I think. */ if (!shortcutKeyHasCtrl(number)) { MenuManager::s_menuItemClicked = true; } runScript(ConfigFile::getInstance()->getMenuScript(number).c_str(), false); } static void runStatement(const TCHAR *statement, bool synchronous, HANDLE completedEvent /* = NULL */, bool allowQueuing /* = false */) { CHECK_INITIALISED(); MenuManager::getInstance()->stopScriptEnabled(true); if (!pythonHandler->runScript(statement, synchronous, allowQueuing, completedEvent, true)) { if (ConfigFile::getInstance()->getSetting(_T("DISABLEPOPUPWARNING")) == _T("0")) { MessageBox(NULL, _T("Another script is currently running. Running two scripts at the same time could produce unpredicable results, and is therefore disabled."), PLUGIN_NAME, 0); } } } static void updatePreviousScript(const TCHAR *filename) { if (g_previousScript == filename) return; g_previousScript = filename; MenuManager *menuManager = MenuManager::getInstance(); menuManager->updatePreviousScript(filename); } static void runScript(const TCHAR *filename, bool synchronous, HANDLE completedEvent /* = NULL */, bool allowQueuing /* = false */) { BYTE keyState[256]; // If the filename is empty, then just ignore it if (!filename || (*filename) == _T('\0')) { // Reset the menuItemClicked, just in case (highly unlikely, but still) MenuManager::s_menuItemClicked = false; return; } ::GetKeyboardState(keyState); // If a menu item was clicked (or assumed to be, see runScript(int)) // and either control held down, and shift + alt are not, then edit the file if ((MenuManager::s_menuItemClicked || previousScript_clicked) && (keyState[VK_CONTROL] & 0x80) && ((keyState[VK_SHIFT] & 0x80) == 0) && ((keyState[VK_MENU] & 0x80) == 0)) { if (!SendMessage(nppData._nppHandle, NPPM_SWITCHTOFILE, 0, reinterpret_cast(WcharMbcsConverter::tchar2tchar(filename).get()))) { SendMessage(nppData._nppHandle, NPPM_DOOPEN, 0, reinterpret_cast(WcharMbcsConverter::tchar2tchar(filename).get())); } } else { CHECK_INITIALISED(); MenuManager::getInstance()->stopScriptEnabled(true); // TODO: Really need to not change this if it's a MSGTOPLUGIN run updatePreviousScript(filename); if (!pythonHandler->runScript(filename, synchronous, allowQueuing, completedEvent)) { if (ConfigFile::getInstance()->getSetting(_T("DISABLEPOPUPWARNING")) == _T("0")) { MessageBox(NULL, _T("Another script is currently running. Running two scripts at the same time could produce unpredicable results, and is therefore disabled."), PLUGIN_NAME, 0); } } } MenuManager::s_menuItemClicked = false; } static void showShortcutDlg() { if (g_shortcutDlg) { g_shortcutDlg->doDialog(); } } static void toggleConsole() { if (g_console) { CHECK_INITIALISED(); if (MenuManager::getInstance()->s_menuItemConsoleChecked) { g_console->hideDialog(); g_bToggleConsoleFlag = false; // this is not necessary but maybe for a future use... } else { g_console->showDialog(); g_bToggleConsoleFlag = true; // this is not necessary but maybe for a future use... } } else { // track the PS console showing/hiding requests even without the full PS initialization // - this fixes the Notepad++ DockingManager-init as it calls this func even before the PS plugin full init at its NPPN_READY g_bToggleConsoleFlag = !g_bToggleConsoleFlag; } } static void ensurePathExists(const tstring& path) { BOOL created = FALSE; tstring createPath(path); if (!::CreateDirectory(createPath.c_str(), NULL)) { std::list pathsToCreate; // Add the deepest directory to the top of the list, so it will be created last pathsToCreate.push_back(createPath); do { createPath.erase(createPath.find_last_of(_T('\\'))); if (!::CreateDirectory(createPath.c_str(), NULL)) { pathsToCreate.push_back(createPath); } else { created = TRUE; } } while (createPath.find(_T('\\')) != tstring::npos && !created); if (created) { for(std::list::reverse_iterator iter = pathsToCreate.rbegin(); iter != pathsToCreate.rend(); iter++) { ::CreateDirectory(iter->c_str(), NULL); } } } else { created = TRUE; } } static void newScript() { OPENFILENAME ofn; memset(&ofn, 0, sizeof(OPENFILENAME)); ofn.lStructSize = sizeof(OPENFILENAME); ofn.hwndOwner = nppData._nppHandle; ensurePathExists(ConfigFile::getInstance()->getUserScriptsDir()); ofn.lpstrInitialDir = ConfigFile::getInstance()->getUserScriptsDir().c_str(); //ofn.lpstrFileTitle = "Choose filename for new script"; ofn.lpstrFile = new TCHAR[MAX_PATH]; ofn.lpstrFile[0] = _T('\0'); ofn.nMaxFile = MAX_PATH; ofn.lpstrDefExt = _T("py"); //lint -e840 Use of nul character in a string literal // This is how it's meant to be used. ofn.lpstrFilter = _T("Python Source Files (*.py)\0*.py\0All Files (*.*)\0*.*\0"); //lint +e840 ofn.nFilterIndex = 1; ofn.Flags = OFN_OVERWRITEPROMPT; if (GetSaveFileName(&ofn)) { HANDLE hFile = CreateFile(ofn.lpstrFile, GENERIC_WRITE, 0, NULL, CREATE_NEW, FILE_ATTRIBUTE_NORMAL, NULL); CloseHandle(hFile); SendMessage(nppData._nppHandle, NPPM_DOOPEN, 0, reinterpret_cast(ofn.lpstrFile)); intptr_t bufferID = SendMessage(nppData._nppHandle, NPPM_GETCURRENTBUFFERID, 0, 0); SendMessage(nppData._nppHandle, NPPM_SETBUFFERLANGTYPE, L_PYTHON, bufferID); } delete [] ofn.lpstrFile; } static void shutdown(void* /* dummy */) { if (pythonHandler) { delete pythonHandler; pythonHandler = NULL; } if (g_console.get()) { g_console.reset(); } for(std::vector::iterator it = g_menuScripts.begin(); it != g_menuScripts.end(); ++it) { delete *it; } for(std::vector< std::pair* >::iterator it = g_toolbarScripts.begin(); it != g_toolbarScripts.end(); ++it) { // Delete the string delete (*(*it)).first; // Destroy the HICON DestroyIcon((*(*it)).second); // Delete the pair delete *it; } MenuManager::deleteInstance(); } static void doHelp() { int which = 0; SendMessage(nppData._nppHandle, NPPM_GETCURRENTSCINTILLA, 0, reinterpret_cast(&which)); HelpController help(nppData._nppHandle, which ? nppData._scintillaSecondHandle : nppData._scintillaMainHandle); help.callHelp(); } static void previousScript() { previousScript_clicked = true; runScript(g_previousScript.c_str(), false); previousScript_clicked = false; } HWND getCurrScintilla() { int which = -1; ::SendMessage( nppData._nppHandle, NPPM_GETCURRENTSCINTILLA, 0, ( LPARAM )&which ); return ( which == 0 ) ? nppData._scintillaMainHandle : nppData._scintillaSecondHandle; }