diff --git a/src/Classes/CollectionInterfaceClass.cpp b/src/Classes/CollectionInterfaceClass.cpp index f2358a4..5b8464e 100644 --- a/src/Classes/CollectionInterfaceClass.cpp +++ b/src/Classes/CollectionInterfaceClass.cpp @@ -6,7 +6,7 @@ CollectionInterface::CollectionInterface(HWND hwndNpp) { _hwndNPP = hwndNpp; _populateNppDirs(); - getListsFromJson(); + _areListsPopulated = getListsFromJson(); }; void CollectionInterface::_populateNppDirs(void) { @@ -228,8 +228,9 @@ std::string CollectionInterface::_xml_unentity(const std::string& text) } #pragma warning ( pop ) -void CollectionInterface::getListsFromJson(void) +bool CollectionInterface::getListsFromJson(void) { + bool didThemeFail = false; auto string2wstring = [](std::string str) { if (str.empty()) return std::wstring(); int wsz = MultiByteToWideChar(CP_UTF8, 0, str.c_str(), static_cast(str.size()), NULL, 0); @@ -242,104 +243,169 @@ void CollectionInterface::getListsFromJson(void) // Process Theme JSON //////////////////////////////// std::vector vcThemeJSON = downloadFileInMemory(L"https://raw.githubusercontent.com/notepad-plus-plus/nppThemes/master/themes/.toc.json"); - nlohmann::json jTheme = nlohmann::json::parse(vcThemeJSON); - std::string v = jTheme.at(0).get(); - for (const auto& item : jTheme.items()) { - std::wstring ws = string2wstring(item.value().get()); - vThemeFiles.push_back(ws.c_str()); + if (vcThemeJSON.empty()) { + // issue#13: do not continue if there's internet/connection problems + return false; // nothing downloaded, so want to know to close the download-dialog to avoid annoying user with useless empty listbox + } + else if (vcThemeJSON[0] != L'[') { + // related to issue#13: if downloadFileInMemory returns "404 Not Found" or similar, don't try to parse as JSON. + // easiest check: if the JSON isn't the expected [...] JSON array, don't continue with the _theme_; + // however, can still move to the UDL section, because that might still work, and since UDL is the primary purpose, it's probably worth it if UDL is working even if Themes aren't. + std::string msg = "Cannot interpret Themes Collection information:\n\n"; + msg += vcThemeJSON.data(); + if (msg.size() > 100) { + msg.resize(100); + msg += "\n..."; + } + ::MessageBoxA(_hwndNPP, msg.c_str(), "CollectionInterface: Download Problems", MB_ICONWARNING); + didThemeFail = true; + } + else { + try + { + nlohmann::json jTheme = nlohmann::json::parse(vcThemeJSON); + std::string v = jTheme.at(0).get(); + for (const auto& item : jTheme.items()) { + std::wstring ws = string2wstring(item.value().get()); + vThemeFiles.push_back(ws.c_str()); + } + } + catch (nlohmann::json::exception& e) { + std::string msg = std::string("JSON Error in Theme data: ") + e.what(); + ::MessageBoxA(_hwndNPP, msg.c_str(), "CollectionInterface: JSON Error", MB_ICONERROR); + didThemeFail = true; + } + catch (std::exception& e) { + std::string msg = std::string("Unrecognized Error in Theme data: ") + e.what(); + ::MessageBoxA(_hwndNPP, msg.c_str(), "CollectionInterface: Unrecognized Error", MB_ICONERROR); + didThemeFail = true; + } } //////////////////////////////// // Process UDL JSON //////////////////////////////// std::vector vcUdlJSON = downloadFileInMemory(L"https://raw.githubusercontent.com/notepad-plus-plus/userDefinedLanguages/refs/heads/master/udl-list.json"); - nlohmann::json jUdl = nlohmann::json::parse(vcUdlJSON); - // for a list, the key() is just the index, and the value() is the sub-object - for (const auto& item : jUdl["UDLs"].items()) { - auto j = item.value(); - std::wstring ws_id_name = string2wstring(j["id-name"].get()); - std::wstring udl_base = L"https://raw.githubusercontent.com/notepad-plus-plus/userDefinedLanguages/master/"; - - // Logic for UDL -> URL - if (j.contains("repository")) { - std::wstring sUDL = L""; - if (j["repository"].is_boolean()) { // URL repo should never be boolean; but if it is, generate default URL - sUDL = udl_base + L"UDLs/" + ws_id_name + L".xml"; - } - if (j["repository"].is_string()) { - std::wstring ws = string2wstring(j["repository"].get()); - if (ws == L"") { - sUDL = udl_base + L"UDLs/" + ws_id_name + L".xml"; - } - else if (ws.find(L"http") == 0) { // if string _starts_ with http or https, it's the full URL - sUDL = ws; - } - } - - // assign into the data structure... - if (sUDL != L"") { - mapUDL[ws_id_name] = sUDL; - } + if (vcUdlJSON.empty()) { + // issue#13: do not continue if there's internet/connection problems + return false; // nothing downloaded, so want to know to close the download-dialog to avoid annoying user with useless empty listbox + } + else if (vcUdlJSON[0] != L'{') { + // related to issue#13: if downloadFileInMemory returns "404 Not Found" or similar, don't try to parse as JSON. + // easiest check: if the JSON isn't the expected [...] JSON array, don't continue with the _theme_; + std::string msg = "Cannot interpret UDL Collection information:\n\n"; + msg += vcUdlJSON.data(); + if (msg.size() > 100) { + msg.resize(100); + msg += "\n..."; } + if (!didThemeFail) + ::MessageBoxA(_hwndNPP, msg.c_str(), "CollectionInterface: Download Problems", MB_ICONWARNING); + return false; // without UDL info, it's not worth displaying the Download Dialog + } + else { + try + { + nlohmann::json jUdl = nlohmann::json::parse(vcUdlJSON); + // for a list, the key() is just the index, and the value() is the sub-object + for (const auto& item : jUdl["UDLs"].items()) { + auto j = item.value(); + std::wstring ws_id_name = string2wstring(j["id-name"].get()); + std::wstring udl_base = L"https://raw.githubusercontent.com/notepad-plus-plus/userDefinedLanguages/master/"; + + // Logic for UDL -> URL + if (j.contains("repository")) { + std::wstring sUDL = L""; + if (j["repository"].is_boolean()) { // URL repo should never be boolean; but if it is, generate default URL + sUDL = udl_base + L"UDLs/" + ws_id_name + L".xml"; + } + if (j["repository"].is_string()) { + std::wstring ws = string2wstring(j["repository"].get()); + if (ws == L"") { + sUDL = udl_base + L"UDLs/" + ws_id_name + L".xml"; + } + else if (ws.find(L"http") == 0) { // if string _starts_ with http or https, it's the full URL + sUDL = ws; + } + } - // Extract display-name - if (j.contains("display-name")) { - std::wstring wdisplay_name = string2wstring(_xml_unentity(j["display-name"].get())); - - // assign into the data structure... - if (wdisplay_name != L"") { - mapDISPLAY[ws_id_name] = wdisplay_name; - revDISPLAY[wdisplay_name] = ws_id_name; - } - } + // assign into the data structure... + if (sUDL != L"") { + mapUDL[ws_id_name] = sUDL; + } + } - // Logic for functionList -> URL - if (j.contains("functionList")) { - std::wstring wsFuncList = L""; - if (j["functionList"].is_boolean() && j["functionList"].get()) { - wsFuncList = udl_base + L"functionList/" + ws_id_name + L".xml"; - } - if (j["functionList"].is_string()) { - std::wstring ws = string2wstring(j["functionList"].get()); + // Extract display-name + if (j.contains("display-name")) { + std::wstring wdisplay_name = string2wstring(_xml_unentity(j["display-name"].get())); - if (ws.find(L"http") == 0) { // if string _starts_ with http or https, it's the full URL - wsFuncList = ws; - } - else { - wsFuncList = udl_base + L"functionList/" + ws + L".xml"; + // assign into the data structure... + if (wdisplay_name != L"") { + mapDISPLAY[ws_id_name] = wdisplay_name; + revDISPLAY[wdisplay_name] = ws_id_name; + } } - } - // assign wsFuncList into the data structure... - if (wsFuncList != L"") { - mapFL[ws_id_name] = wsFuncList; - } - } + // Logic for functionList -> URL + if (j.contains("functionList")) { + std::wstring wsFuncList = L""; + if (j["functionList"].is_boolean() && j["functionList"].get()) { + wsFuncList = udl_base + L"functionList/" + ws_id_name + L".xml"; + } + if (j["functionList"].is_string()) { + std::wstring ws = string2wstring(j["functionList"].get()); + + if (ws.find(L"http") == 0) { // if string _starts_ with http or https, it's the full URL + wsFuncList = ws; + } + else { + wsFuncList = udl_base + L"functionList/" + ws + L".xml"; + } + } - // Logic for autoCompletion -> URL - if (j.contains("autoCompletion")) { - std::wstring wsAutoComp = L""; - if (j["autoCompletion"].is_boolean()) { - wsAutoComp = udl_base + L"autoCompletion/" + ws_id_name + L".xml"; - } - if (j["autoCompletion"].is_string()) { - std::wstring ws = string2wstring(j["autoCompletion"].get()); - if (ws.find(L"http") == 0) { - wsAutoComp = ws; - } - else { - wsAutoComp = udl_base + L"autoCompletion/" + ws + L".xml"; + // assign wsFuncList into the data structure... + if (wsFuncList != L"") { + mapFL[ws_id_name] = wsFuncList; + } } - } - // assign sAutoComp into the data structure... - if (wsAutoComp != L"") { - mapAC[ws_id_name] = wsAutoComp; + // Logic for autoCompletion -> URL + if (j.contains("autoCompletion")) { + std::wstring wsAutoComp = L""; + if (j["autoCompletion"].is_boolean()) { + wsAutoComp = udl_base + L"autoCompletion/" + ws_id_name + L".xml"; + } + if (j["autoCompletion"].is_string()) { + std::wstring ws = string2wstring(j["autoCompletion"].get()); + if (ws.find(L"http") == 0) { + wsAutoComp = ws; + } + else { + wsAutoComp = udl_base + L"autoCompletion/" + ws + L".xml"; + } + } + + // assign sAutoComp into the data structure... + if (wsAutoComp != L"") { + mapAC[ws_id_name] = wsAutoComp; + } + } } } + catch (nlohmann::json::exception& e) { + std::string msg = std::string("JSON Error in UDL data: ") + e.what(); + ::MessageBoxA(_hwndNPP, msg.c_str(), "CollectionInterface: JSON Error", MB_ICONERROR); + return false; // without UDL info, it's not worth displaying the Download Dialog + } + catch (std::exception& e) { + std::string msg = std::string("Unrecognized Error in UDL data: ") + e.what(); + ::MessageBoxA(_hwndNPP, msg.c_str(), "CollectionInterface: Unrecognized Error", MB_ICONERROR); + return false; // without UDL info, it's not worth displaying the Download Dialog + } } - return; + // if it makes it here, there is enough data to be worth displaying the dialog + return true; } std::wstring& CollectionInterface::_wsDeleteTrailingNulls(std::wstring& str) @@ -371,7 +437,7 @@ bool CollectionInterface::_is_dir_writable(const std::wstring& path) std::wstring CollectionInterface::getWritableTempDir(void) { // first try the system TEMP - std::wstring tempDir(MAX_PATH+1, L'\0'); + std::wstring tempDir(MAX_PATH + 1, L'\0'); GetTempPath(MAX_PATH + 1, const_cast(tempDir.data())); _wsDeleteTrailingNulls(tempDir); @@ -418,5 +484,5 @@ bool CollectionInterface::ask_overwrite_if_exists(const std::wstring& path) if (!PathFileExists(path.c_str())) return true; // if file doesn't exist, it's okay to "overwrite" nothing ;-) std::wstring msg = L"The path\r\n" + path + L"\r\nalready exists. Should I overwrite it?"; int ans = ::MessageBox(_hwndNPP, msg.c_str(), L"Overwrite File?", MB_YESNO); - return ans==IDYES; + return ans == IDYES; } diff --git a/src/Classes/CollectionInterfaceClass.h b/src/Classes/CollectionInterfaceClass.h index 7445e2a..5dc8352 100644 --- a/src/Classes/CollectionInterfaceClass.h +++ b/src/Classes/CollectionInterfaceClass.h @@ -32,7 +32,7 @@ class CollectionInterface { //bool downloadFileToDisk(const std::wstring& url, const std::string& path); //bool downloadFileToDisk(const std::string& url, const std::wstring& path); bool downloadFileToDisk(const std::wstring& url, const std::wstring& path); - void getListsFromJson(void); + bool getListsFromJson(void); // getter methods std::wstring nppCfgDir(void) { return _nppCfgDir; }; @@ -46,6 +46,7 @@ class CollectionInterface { bool isFunctionListDirWritable(void) { return _is_dir_writable(_nppCfgFunctionListDir); }; bool isAutoCompletionDirWritable(void) { return _is_dir_writable(_nppCfgAutoCompletionDir); }; bool isThemesDirWritable(void) { return _is_dir_writable(_nppCfgThemesDir); }; + bool areListsPopulated(void) { return _areListsPopulated; }; // if the chosen directory isn't writable, need to be able to use a directory that _is_ writable // as a TempDir, and then will need to use runas to copy from the TempDir to the real dir. @@ -69,4 +70,5 @@ class CollectionInterface { _nppCfgThemesDir; HWND _hwndNPP; + bool _areListsPopulated; }; diff --git a/src/Dialogs/CollectionInterfaceDialog.cpp b/src/Dialogs/CollectionInterfaceDialog.cpp index 6eb71b1..d07c544 100644 --- a/src/Dialogs/CollectionInterfaceDialog.cpp +++ b/src/Dialogs/CollectionInterfaceDialog.cpp @@ -97,7 +97,14 @@ INT_PTR CALLBACK ciDlgProc(HWND hwndDlg, UINT uMsg, WPARAM wParam, LPARAM lParam Edit_SetText(GetDlgItem(hwndDlg, IDC_CI_PROGRESSLBL), L"READY"); pobjCI = new CollectionInterface(hParent); - //pobjCI->getListsFromJson(); + if (!pobjCI->areListsPopulated()) { + EndDialog(hwndDlg, 0); + DestroyWindow(hwndDlg); + g_hwndCIDlg = nullptr; + delete pobjCI; + pobjCI = NULL; + return true; + } _populate_file_cbx(hwndDlg, pobjCI->mapUDL, pobjCI->mapDISPLAY); // Dark Mode Subclass and Theme: needs to go _after_ all the controls have been initialized @@ -105,7 +112,7 @@ INT_PTR CALLBACK ciDlgProc(HWND hwndDlg, UINT uMsg, WPARAM wParam, LPARAM lParam LRESULT darkdialogVersion = MAKELONG(540, 8); // NPPM_GETDARKMODECOLORS requires 8.4.1 and NPPM_DARKMODESUBCLASSANDTHEME requires 8.5.4 LRESULT localsubclassVersion = MAKELONG(810, 8); // from 8.540 to 8.810 (at least), need to do local subclassing because of tab control g_IsDarkMode = (bool)::SendMessage(nppData._nppHandle, NPPM_ISDARKMODEENABLED, 0, 0); - if (g_IsDarkMode && (nppVersion>=darkdialogVersion)) { + if (g_IsDarkMode && (nppVersion >= darkdialogVersion)) { ::SendMessage(nppData._nppHandle, NPPM_GETDARKMODECOLORS, sizeof(NppDarkMode::Colors), reinterpret_cast(&myColors)); myBrushes.change(myColors); myPens.change(myColors); @@ -219,13 +226,16 @@ INT_PTR CALLBACK ciDlgProc(HWND hwndDlg, UINT uMsg, WPARAM wParam, LPARAM lParam { std::wstring wsCategory = _get_tab_category_wstr(hwndDlg, IDC_CI_TABCTRL); LRESULT selectedFileIndex = ::SendDlgItemMessage(hwndDlg, IDC_CI_COMBO_FILE, LB_GETCURSEL, 0, 0); - switch (selectedFileIndex) { - case CB_ERR: - ::MessageBox(NULL, L"Could not understand FILE combobox; sorry", L"Download Error", MB_ICONERROR); - return true; + if (selectedFileIndex == CB_ERR) { + ::MessageBox(NULL, L"Could not understand name selection; sorry", L"Download Error", MB_ICONERROR); + return true; } LRESULT needFileLen = ::SendDlgItemMessage(hwndDlg, IDC_CI_COMBO_FILE, LB_GETTEXTLEN, selectedFileIndex, 0); + if (needFileLen == LB_ERR) { + ::MessageBox(NULL, L"Could not understand name selection; sorry", L"Download Error", MB_ICONERROR); + return true; + } std::wstring wsFilename(needFileLen, 0); ::SendDlgItemMessage(hwndDlg, IDC_CI_COMBO_FILE, LB_GETTEXT, selectedFileIndex, reinterpret_cast(wsFilename.data())); @@ -334,10 +344,12 @@ INT_PTR CALLBACK ciDlgProc(HWND hwndDlg, UINT uMsg, WPARAM wParam, LPARAM lParam } // update progress bar - ::SendDlgItemMessage(hwndDlg, IDC_CI_PROGRESSBAR, PBM_SETPOS, 100 * count / total, 0); wchar_t wcDLPCT[256]; swprintf_s(wcDLPCT, L"Downloading %d%%", 100 * count / total); - Edit_SetText(GetDlgItem(hwndDlg, IDC_CI_PROGRESSLBL), wcDLPCT); + if (didDownload) { + ::SendDlgItemMessage(hwndDlg, IDC_CI_PROGRESSBAR, PBM_SETPOS, 100 * count / total, 0); + Edit_SetText(GetDlgItem(hwndDlg, IDC_CI_PROGRESSLBL), wcDLPCT); + } // also download AC and FL, if applicable std::vector xtra = { L"AC", L"FL" }; @@ -375,15 +387,24 @@ INT_PTR CALLBACK ciDlgProc(HWND hwndDlg, UINT uMsg, WPARAM wParam, LPARAM lParam } } // update progress bar - ::SendDlgItemMessage(hwndDlg, IDC_CI_PROGRESSBAR, PBM_SETPOS, 100 * count / total, 0); swprintf_s(wcDLPCT, L"Downloading %d%%", 100 * count / total); - Edit_SetText(GetDlgItem(hwndDlg, IDC_CI_PROGRESSLBL), wcDLPCT); + if (didDownload) { + ::SendDlgItemMessage(hwndDlg, IDC_CI_PROGRESSBAR, PBM_SETPOS, 100 * count / total, 0); + Edit_SetText(GetDlgItem(hwndDlg, IDC_CI_PROGRESSLBL), wcDLPCT); + } } // Final update of progress bar: 100% - ::SendDlgItemMessage(hwndDlg, IDC_CI_PROGRESSBAR, PBM_SETPOS, 100, 0); - swprintf_s(wcDLPCT, L"Downloading %d%% [DONE]", 100); - Edit_SetText(GetDlgItem(hwndDlg, IDC_CI_PROGRESSLBL), wcDLPCT); + if (didDownload) { + ::SendDlgItemMessage(hwndDlg, IDC_CI_PROGRESSBAR, PBM_SETPOS, 100, 0); + swprintf_s(wcDLPCT, L"Downloading %d%% [DONE]", 100); + Edit_SetText(GetDlgItem(hwndDlg, IDC_CI_PROGRESSLBL), wcDLPCT); + } + else { + ::SendDlgItemMessage(hwndDlg, IDC_CI_PROGRESSBAR, PBM_SETPOS, 0, 0); + swprintf_s(wcDLPCT, L"Nothing to Download. [DONE]"); + Edit_SetText(GetDlgItem(hwndDlg, IDC_CI_PROGRESSLBL), wcDLPCT); + } } return true; case IDC_CI_HELPBTN: