This commit is contained in:
恍兮惚兮 2024-04-21 17:38:47 +08:00
parent e9229a0a73
commit 792041595c
43 changed files with 5044 additions and 52 deletions

View File

@ -33,15 +33,15 @@ if(NOT DEFINED LANGUAGE)
endif()
if(NOT DEFINED WINXP)
set(WINXP "")
set(WINXPAPP "")
else()
set(WINXP "_winxp")
set(WINXPAPP "_winxp")
endif()
add_definitions(-DLANGUAGE=${LANGUAGE})
set(CMAKE_FINAL_OUTPUT_DIRECTORY ${CMAKE_SOURCE_DIR}/builds/${CMAKE_BUILD_TYPE}_x${bitappendix}_${LANGUAGE}${WINXP})
set(binary_out_putpath ${CMAKE_SOURCE_DIR}/builds/${CMAKE_BUILD_TYPE}_${LANGUAGE}${WINXP})
set(CMAKE_FINAL_OUTPUT_DIRECTORY ${CMAKE_SOURCE_DIR}/builds/${CMAKE_BUILD_TYPE}_x${bitappendix}_${LANGUAGE}${WINXPAPP})
set(binary_out_putpath ${CMAKE_SOURCE_DIR}/builds/${CMAKE_BUILD_TYPE}_${LANGUAGE}${WINXPAPP})
#set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY $<1:${CMAKE_FINAL_OUTPUT_DIRECTORY}>)
set(CMAKE_LIBRARY_OUTPUT_DIRECTORY $<1:${binary_out_putpath}>)
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY $<1:${binary_out_putpath}>)
@ -55,7 +55,7 @@ include(generate_product_version)
set(VERSION_MAJOR 3)
set(VERSION_MINOR 0)
set(VERSION_PATCH 1)
set(VERSION_PATCH 2)
set(VERSION_REVISION 0)
add_subdirectory(include)

View File

@ -9,9 +9,9 @@ else()
set(collector "enginecollection32.cpp")
endif()
string(REPLACE ";" ".cpp;${enginepath}/" enginessrc "${enginessrc}")
message("${enginessrc}")
#message("${enginessrc}")
set(enginessrc "${enginepath}/${enginessrc}.cpp")
message("${enginessrc}")
#message("${enginessrc}")
set_source_files_properties(${enginessrc} PROPERTIES SOURCE_ENCODING "UTF-8")
set(texthook_src

View File

@ -1,4 +1,4 @@
add_executable(LunaHost WIN32 confighelper.cpp controls.cpp main.cpp processlistwindow.cpp LunaHost.cpp window.cpp luna.rc pluginmanager.cpp Plugin/pluginexample.cpp QtLoader_inline.cpp app.manifest ${versioninfohost})
add_executable(LunaHost WIN32 confighelper.cpp controls.cpp main.cpp processlistwindow.cpp LunaHost.cpp window.cpp luna.rc pluginmanager.cpp Plugin/extensionimpl.cpp Plugin/copyclipboard.cpp QtLoader_inline.cpp app.manifest ${versioninfohost})
target_precompile_headers(LunaHost REUSE_FROM pch)
set_target_properties(LunaHost PROPERTIES OUTPUT_NAME "LunaHost${bitappendix}")
target_link_libraries(LunaHost comctl32 winhttp version pch host ${YY_Thunks_for_WinXP})

View File

@ -1,9 +1,3 @@
if(0) #exe
add_library(ToClipboard MODULE pluginexample.cpp)
target_precompile_headers(ToClipboard REUSE_FROM pch)
set_target_properties(ToClipboard PROPERTIES LIBRARY_OUTPUT_DIRECTORY "${CMAKE_LIBRARY_OUTPUT_DIRECTORY}/plugin${bitappendix}")
endif()
if(0)
include(QtUtils.cmake)
msvc_registry_search()
@ -17,3 +11,12 @@ if(Qt5_DIR)
set_target_properties(QtLoader PROPERTIES LIBRARY_OUTPUT_DIRECTORY "${CMAKE_LIBRARY_OUTPUT_DIRECTORY}/plugin${bitappendix}")
endif()
endif()
if(NOT DEFINED WINXP)
include(QtUtils.cmake)
msvc_registry_search()
if(Qt5_DIR)
find_qt5(Core Widgets WebSockets)
add_subdirectory(extensions)
endif()
endif()

View File

@ -0,0 +1,11 @@
#include "extension.h"
bool sendclipboarddata(const std::wstring&text,HWND hwnd);
bool ProcessSentence(std::wstring& sentence, SentenceInfo sentenceInfo)
{
if (sentenceInfo["current select"] && sentenceInfo["process id"] != 0 &&sentenceInfo["toclipboard"])
{
sendclipboarddata(sentence,(HWND)sentenceInfo["HostHWND"]);
}
return false;
}

View File

@ -1,6 +1,5 @@
#include<Windows.h>
#ifndef LUNA_PLUGIN_DEF_H
#define LUNA_PLUGIN_DEF_H
#pragma once
struct InfoForExtension
{
const char* name;
@ -17,6 +16,6 @@ struct SentenceInfo
return *(int*)0xDEAD = 0; // gives better error message than alternatives
}
};
typedef wchar_t* (*OnNewSentence_t)(wchar_t*, const InfoForExtension*);
#endif
struct SKIP {};
inline void Skip() { throw SKIP(); }

View File

@ -0,0 +1,33 @@
#include "extension.h"
bool ProcessSentence(std::wstring& sentence, SentenceInfo sentenceInfo);
/*
You shouldn't mess with this or even look at it unless you're certain you know what you're doing.
Param sentence: pointer to sentence received by Textractor (UTF-16).
This can be modified. Textractor uses the modified sentence for future processing and display. If empty (starts with null terminator), Textractor will destroy it.
Textractor will display the sentence after all extensions have had a chance to process and/or modify it.
The buffer is allocated using HeapAlloc(). If you want to make it larger, please use HeapReAlloc().
Param sentenceInfo: pointer to array containing misc info about the sentence. End of array is marked with name being nullptr.
Return value: the buffer used for the sentence. Remember to return a new pointer if HeapReAlloc() gave you one.
This function may be run concurrently with itself: please make sure it's thread safe.
It will not be run concurrently with DllMain.
*/
extern "C" __declspec(dllexport) wchar_t* OnNewSentence(wchar_t* sentence, const InfoForExtension* sentenceInfo)
{
try
{
std::wstring sentenceCopy(sentence);
int oldSize = sentenceCopy.size();
if (ProcessSentence(sentenceCopy, SentenceInfo{ sentenceInfo }))
{
if (sentenceCopy.size() > oldSize) sentence = (wchar_t*)HeapReAlloc(GetProcessHeap(), HEAP_GENERATE_EXCEPTIONS, sentence, (sentenceCopy.size() + 1) * sizeof(wchar_t));
wcscpy_s(sentence, sentenceCopy.size() + 1, sentenceCopy.c_str());
}
}
catch (SKIP)
{
*sentence = L'\0';
}
return sentence;
}

View File

@ -0,0 +1,52 @@
cmake_policy(SET CMP0037 OLD)
include_directories(../)
add_library(extpch text.cpp)
target_precompile_headers(extpch PUBLIC extpch.h)
set(disttarget "${CMAKE_SOURCE_DIR}/builds/plugin${bitappendix}")
message(${disttarget})
function(add_library_and_link_target TARGET_NAME)
add_library(${TARGET_NAME} MODULE ${ARGN} ../extensionimpl.cpp)
target_precompile_headers(${TARGET_NAME} REUSE_FROM extpch)
target_link_libraries(${TARGET_NAME} PRIVATE extpch shell32 winhttp Qt5::Widgets Qt5::WebSockets)
set_target_properties(${TARGET_NAME} PROPERTIES
LIBRARY_OUTPUT_DIRECTORY ${disttarget}
LIBRARY_OUTPUT_DIRECTORY_DEBUG ${disttarget}
LIBRARY_OUTPUT_DIRECTORY_RELEASE ${disttarget}
)
endfunction()
add_library_and_link_target(Bing\ Translate bingtranslate.cpp translatewrapper.cpp network.cpp)
#add_library_and_link_target(Copy\ to\ Clipboard copyclipboard.cpp)
add_library_and_link_target(DeepL\ Translate deepltranslate.cpp translatewrapper.cpp network.cpp)
add_library_and_link_target(DevTools\ DeepL\ Translate devtoolsdeepltranslate.cpp devtools.cpp translatewrapper.cpp network.cpp)
add_library_and_link_target(DevTools\ Papago\ Translate devtoolspapagotranslate.cpp devtools.cpp translatewrapper.cpp network.cpp)
add_library_and_link_target(DevTools\ Systran\ Translate devtoolssystrantranslate.cpp devtools.cpp translatewrapper.cpp network.cpp)
add_library_and_link_target(Extra\ Newlines extranewlines.cpp)
add_library_and_link_target(Extra\ Window extrawindow.cpp)
add_library_and_link_target(Google\ Translate googletranslate.cpp translatewrapper.cpp network.cpp)
add_library_and_link_target(Regex\ Filter regexfilter.cpp)
add_library_and_link_target(Regex\ Replacer regexreplacer.cpp)
add_library_and_link_target(Remove\ Repeated\ Characters removerepeatchar.cpp)
add_library_and_link_target(Remove\ Repeated\ Phrases removerepeatphrase.cpp)
add_library_and_link_target(Remove\ Repeated\ Phrases\ 2 removerepeatphrase2.cpp)
add_library_and_link_target(Remove\ 30\ Repeated\ Sentences removerepeatsentence.cpp)
add_library_and_link_target(Replacer replacer.cpp)
add_library_and_link_target(Styler styler.cpp)
add_library_and_link_target(Thread\ Linker threadlinker.cpp)
if (NOT EXISTS ${disttarget}/Qt5WebSockets.dll AND NOT EXISTS ${disttarget}/Qt5WebSocketsd.dll)
add_custom_command(TARGET DevTools\ DeepL\ Translate
POST_BUILD
COMMAND ${CMAKE_COMMAND} -E remove_directory "${CMAKE_CURRENT_BINARY_DIR}/windeployqt"
COMMAND set PATH=%PATH%$<SEMICOLON>${qt5_install_prefix}/bin
COMMAND Qt5::windeployqt --dir ${disttarget} "${disttarget}/DevTools\ DeepL\ Translate.dll"
)
endif()

View File

@ -0,0 +1,241 @@
#include "qtcommon.h"
#include "translatewrapper.h"
#include "network.h"
extern const wchar_t* TRANSLATION_ERROR;
const char* TRANSLATION_PROVIDER = "Bing Translate";
const char* GET_API_KEY_FROM = "https://www.microsoft.com/en-us/translator/business/trial/#get-started";
extern const QStringList languagesTo
{
"Afrikaans",
"Albanian",
"Amharic",
"Arabic",
"Armenian",
"Assamese",
"Azerbaijani",
"Bangla",
"Bosnian (Latin)",
"Bulgarian",
"Cantonese (Traditional)",
"Catalan",
"Chinese (Simplified)",
"Chinese (Traditional)",
"Croatian",
"Czech",
"Danish",
"Dari",
"Dutch",
"English",
"Estonian",
"Fijian",
"Filipino",
"Finnish",
"French",
"French (Canada)",
"German",
"Greek",
"Gujarati",
"Haitian Creole",
"Hebrew",
"Hindi",
"Hmong Daw",
"Hungarian",
"Icelandic",
"Indonesian",
"Inuktitut",
"Irish",
"Italian",
"Japanese",
"Kannada",
"Kazakh",
"Khmer",
"Klingon",
"Korean",
"Kurdish (Central)",
"Kurdish (Northern)",
"Lao",
"Latvian",
"Lithuanian",
"Malagasy",
"Malay",
"Malayalam",
"Maltese",
"Maori",
"Marathi",
"Myanmar",
"Nepali",
"Norwegian",
"Odia",
"Pashto",
"Persian",
"Polish",
"Portuguese (Brazil)",
"Portuguese (Portugal)",
"Punjabi",
"Queretaro Otomi",
"Romanian",
"Russian",
"Samoan",
"Serbian (Cyrillic)",
"Serbian (Latin)",
"Slovak",
"Slovenian",
"Spanish",
"Swahili",
"Swedish",
"Tahitian",
"Tamil",
"Telugu",
"Thai",
"Tigrinya",
"Tongan",
"Turkish",
"Ukrainian",
"Urdu",
"Vietnamese",
"Welsh",
"Yucatec Maya"
}, languagesFrom = languagesTo;
extern const std::unordered_map<std::wstring, std::wstring> codes
{
{ { L"Afrikaans" }, { L"af" } },
{ { L"Albanian" }, { L"sq" } },
{ { L"Amharic" }, { L"am" } },
{ { L"Arabic" }, { L"ar" } },
{ { L"Armenian" }, { L"hy" } },
{ { L"Assamese" }, { L"as" } },
{ { L"Azerbaijani" }, { L"az" } },
{ { L"Bangla" }, { L"bn" } },
{ { L"Bosnian (Latin)" }, { L"bs" } },
{ { L"Bulgarian" }, { L"bg" } },
{ { L"Cantonese (Traditional)" }, { L"yue" } },
{ { L"Catalan" }, { L"ca" } },
{ { L"Chinese (Simplified)" }, { L"zh-Hans" } },
{ { L"Chinese (Traditional)" }, { L"zh-Hant" } },
{ { L"Croatian" }, { L"hr" } },
{ { L"Czech" }, { L"cs" } },
{ { L"Danish" }, { L"da" } },
{ { L"Dari" }, { L"prs" } },
{ { L"Dutch" }, { L"nl" } },
{ { L"English" }, { L"en" } },
{ { L"Estonian" }, { L"et" } },
{ { L"Fijian" }, { L"fj" } },
{ { L"Filipino" }, { L"fil" } },
{ { L"Finnish" }, { L"fi" } },
{ { L"French" }, { L"fr" } },
{ { L"French (Canada)" }, { L"fr-ca" } },
{ { L"German" }, { L"de" } },
{ { L"Greek" }, { L"el" } },
{ { L"Gujarati" }, { L"gu" } },
{ { L"Haitian Creole" }, { L"ht" } },
{ { L"Hebrew" }, { L"he" } },
{ { L"Hindi" }, { L"hi" } },
{ { L"Hmong Daw" }, { L"mww" } },
{ { L"Hungarian" }, { L"hu" } },
{ { L"Icelandic" }, { L"is" } },
{ { L"Indonesian" }, { L"id" } },
{ { L"Inuktitut" }, { L"iu" } },
{ { L"Irish" }, { L"ga" } },
{ { L"Italian" }, { L"it" } },
{ { L"Japanese" }, { L"ja" } },
{ { L"Kannada" }, { L"kn" } },
{ { L"Kazakh" }, { L"kk" } },
{ { L"Khmer" }, { L"km" } },
{ { L"Klingon" }, { L"tlh-Latn" } },
{ { L"Korean" }, { L"ko" } },
{ { L"Kurdish (Central)" }, { L"ku" } },
{ { L"Kurdish (Northern)" }, { L"kmr" } },
{ { L"Lao" }, { L"lo" } },
{ { L"Latvian" }, { L"lv" } },
{ { L"Lithuanian" }, { L"lt" } },
{ { L"Malagasy" }, { L"mg" } },
{ { L"Malay" }, { L"ms" } },
{ { L"Malayalam" }, { L"ml" } },
{ { L"Maltese" }, { L"mt" } },
{ { L"Maori" }, { L"mi" } },
{ { L"Marathi" }, { L"mr" } },
{ { L"Myanmar" }, { L"my" } },
{ { L"Nepali" }, { L"ne" } },
{ { L"Norwegian" }, { L"nb" } },
{ { L"Odia" }, { L"or" } },
{ { L"Pashto" }, { L"ps" } },
{ { L"Persian" }, { L"fa" } },
{ { L"Polish" }, { L"pl" } },
{ { L"Portuguese (Brazil)" }, { L"pt" } },
{ { L"Portuguese (Portugal)" }, { L"pt-pt" } },
{ { L"Punjabi" }, { L"pa" } },
{ { L"Queretaro Otomi" }, { L"otq" } },
{ { L"Romanian" }, { L"ro" } },
{ { L"Russian" }, { L"ru" } },
{ { L"Samoan" }, { L"sm" } },
{ { L"Serbian (Cyrillic)" }, { L"sr-Cyrl" } },
{ { L"Serbian (Latin)" }, { L"sr-Latn" } },
{ { L"Slovak" }, { L"sk" } },
{ { L"Slovenian" }, { L"sl" } },
{ { L"Spanish" }, { L"es" } },
{ { L"Swahili" }, { L"sw" } },
{ { L"Swedish" }, { L"sv" } },
{ { L"Tahitian" }, { L"ty" } },
{ { L"Tamil" }, { L"ta" } },
{ { L"Telugu" }, { L"te" } },
{ { L"Thai" }, { L"th" } },
{ { L"Tigrinya" }, { L"ti" } },
{ { L"Tongan" }, { L"to" } },
{ { L"Turkish" }, { L"tr" } },
{ { L"Ukrainian" }, { L"uk" } },
{ { L"Urdu" }, { L"ur" } },
{ { L"Vietnamese" }, { L"vi" } },
{ { L"Welsh" }, { L"cy" } },
{ { L"Yucatec Maya" }, { L"yua" } },
{ { L"?" }, { L"auto-detect" } }
};
bool translateSelectedOnly = false, useRateLimiter = true, rateLimitSelected = false, useCache = true, useFilter = true;
int tokenCount = 30, rateLimitTimespan = 60000, maxSentenceSize = 1000;
std::pair<bool, std::wstring> Translate(const std::wstring& text, TranslationParam tlp)
{
if (!tlp.authKey.empty())
{
std::wstring translateFromComponent = tlp.translateFrom == L"?" ? L"" : L"&from=" + codes.at(tlp.translateFrom);
if (HttpRequest httpRequest{
L"Mozilla/5.0 Textractor",
L"api.cognitive.microsofttranslator.com",
L"POST",
FormatString(L"/translate?api-version=3.0&to=%s%s", codes.at(tlp.translateTo), translateFromComponent).c_str(),
FormatString(R"([{"text":"%s"}])", JSON::Escape(WideStringToString(text))),
FormatString(L"Content-Type: application/json; charset=UTF-8\r\nOcp-Apim-Subscription-Key:%s", tlp.authKey).c_str()
})
if (auto translation = Copy(JSON::Parse(httpRequest.response)[0][L"translations"][0][L"text"].String())) return { true, translation.value() };
else return { false, FormatString(L"%s: %s", TRANSLATION_ERROR, httpRequest.response) };
else return { false, FormatString(L"%s (code=%u)", TRANSLATION_ERROR, httpRequest.errorCode) };
}
static std::atomic<int> i = 0;
static Synchronized<std::wstring> token;
if (token->empty()) if (HttpRequest httpRequest{ L"Mozilla/5.0 Textractor", L"www.bing.com", L"GET", L"translator" })
{
std::wstring tokenBuilder;
if (auto tokenPos = httpRequest.response.find(L"[" + std::to_wstring(time(nullptr) / 100)); tokenPos != std::string::npos)
tokenBuilder = FormatString(L"&key=%s&token=%s", httpRequest.response.substr(tokenPos + 1, 13), httpRequest.response.substr(tokenPos + 16, 32));
if (auto tokenPos = httpRequest.response.find(L"IG:\""); tokenPos != std::string::npos)
tokenBuilder += L"&IG=" + httpRequest.response.substr(tokenPos + 4, 32);
if (auto tokenPos = httpRequest.response.find(L"data-iid=\""); tokenPos != std::string::npos)
tokenBuilder += L"&IID=" + httpRequest.response.substr(tokenPos + 10, 15);
if (!tokenBuilder.empty()) token->assign(tokenBuilder);
else return { false, FormatString(L"%s: %s\ntoken not found", TRANSLATION_ERROR, httpRequest.response) };
}
else return { false, FormatString(L"%s: could not acquire token", TRANSLATION_ERROR) };
if (HttpRequest httpRequest{
L"Mozilla/5.0 Textractor",
L"www.bing.com",
L"POST",
FormatString(L"/ttranslatev3?fromLang=%s&to=%s&text=%s%s.%d", codes.at(tlp.translateFrom), codes.at(tlp.translateTo), Escape(text), token.Copy(), i++).c_str()
})
if (auto translation = Copy(JSON::Parse(httpRequest.response)[0][L"translations"][0][L"text"].String())) return { true, translation.value() };
else return { false, FormatString(L"%s (token=%s): %s", TRANSLATION_ERROR, std::exchange(token.Acquire().contents, L""), httpRequest.response) };
else return { false, FormatString(L"%s (code=%u)", TRANSLATION_ERROR, httpRequest.errorCode) };
}

View File

@ -0,0 +1,57 @@
#pragma once
#include <istream>
template <typename C, int delimiterCount, int blockSize = 0x1000 / sizeof(C)> // windows file block size
class BlockMarkupIterator
{
public:
BlockMarkupIterator(const std::istream& stream, const std::basic_string_view<C>(&delimiters)[delimiterCount]) : streambuf(*stream.rdbuf())
{
std::copy_n(delimiters, delimiterCount, this->delimiters.begin());
}
std::optional<std::array<std::basic_string<C>, delimiterCount>> Next()
{
std::array<std::basic_string<C>, delimiterCount> results;
Find(delimiters[0], true);
for (int i = 0; i < delimiterCount; ++i)
{
const auto delimiter = i + 1 < delimiterCount ? delimiters[i + 1] : end;
if (auto found = Find(delimiter, false)) results[i] = std::move(found.value());
else return {};
}
return results;
}
private:
std::optional<std::basic_string<C>> Find(std::basic_string_view<C> delimiter, bool discard)
{
for (int i = 0; ;)
{
int pos = buffer.find(delimiter, i);
if (pos != std::string::npos)
{
auto result = !discard ? std::optional(std::basic_string(buffer.begin(), buffer.begin() + pos)) : std::nullopt;
buffer.erase(buffer.begin(), buffer.begin() + pos + delimiter.size());
return result;
}
int oldSize = buffer.size();
buffer.resize(oldSize + blockSize);
if (!streambuf.sgetn((char*)(buffer.data() + oldSize), blockSize * sizeof(C))) return {};
i = max(0, oldSize - (int)delimiter.size());
if (discard)
{
buffer.erase(0, i);
i = 0;
}
}
}
static constexpr C endImpl[5] = { '|', 'E', 'N', 'D', '|' };
static constexpr std::basic_string_view<C> end{ endImpl, 5 };
std::basic_streambuf<char>& streambuf;
std::basic_string<C> buffer;
std::array<std::basic_string_view<C>, delimiterCount> delimiters;
};

View File

@ -0,0 +1,180 @@
#include "qtcommon.h"
#include "translatewrapper.h"
#include "network.h"
#include <random>
extern const wchar_t* TRANSLATION_ERROR;
const char* TRANSLATION_PROVIDER = "DeepL Translate";
const char* GET_API_KEY_FROM = "https://www.deepl.com/pro.html#developer";
extern const QStringList languagesTo
{
"Bulgarian",
"Chinese (Simplified)",
"Czech",
"Danish",
"Dutch",
"English (American)",
"English (British)",
"Estonian",
"Finnish",
"French",
"German",
"Greek",
"Hungarian",
"Indonesian",
"Italian",
"Japanese",
"Latvian",
"Lithuanian",
"Polish",
"Portuguese (Brazil)",
"Portuguese (Portugal)",
"Romanian",
"Russian",
"Slovak",
"Slovenian",
"Spanish",
"Swedish",
"Turkish"
},
languagesFrom
{
"Bulgarian",
"Chinese",
"Czech",
"Danish",
"Dutch",
"English",
"Estonian",
"Finnish",
"French",
"German",
"Greek",
"Hungarian",
"Indonesian",
"Italian",
"Japanese",
"Latvian",
"Lithuanian",
"Polish",
"Portuguese",
"Romanian",
"Russian",
"Slovak",
"Slovenian",
"Spanish",
"Swedish",
"Turkish"
};
extern const std::unordered_map<std::wstring, std::wstring> codes
{
{ { L"Bulgarian" }, { L"BG" } },
{ { L"Chinese" }, { L"ZH" } },
{ { L"Chinese (Simplified)" }, { L"ZH" } },
{ { L"Czech" }, { L"CS" } },
{ { L"Danish" }, { L"DA" } },
{ { L"Dutch" }, { L"NL" } },
{ { L"English" }, { L"EN" } },
{ { L"English (American)" }, { L"EN-US" } },
{ { L"English (British)" }, { L"EN-GB" } },
{ { L"Estonian" }, { L"ET" } },
{ { L"Finnish" }, { L"FI" } },
{ { L"French" }, { L"FR" } },
{ { L"German" }, { L"DE" } },
{ { L"Greek" }, { L"EL" } },
{ { L"Hungarian" }, { L"HU" } },
{ { L"Indonesian" }, { L"ID" } },
{ { L"Italian" }, { L"IT" } },
{ { L"Japanese" }, { L"JA" } },
{ { L"Latvian" }, { L"LV" } },
{ { L"Lithuanian" }, { L"LT" } },
{ { L"Polish" }, { L"PL" } },
{ { L"Portuguese" }, { L"PT" } },
{ { L"Portuguese (Brazil)" }, { L"PT-BR" } },
{ { L"Portuguese (Portugal)" }, { L"PT-PT" } },
{ { L"Romanian" }, { L"RO" } },
{ { L"Russian" }, { L"RU" } },
{ { L"Slovak" }, { L"SK" } },
{ { L"Slovenian" }, { L"SL" } },
{ { L"Spanish" }, { L"ES" } },
{ { L"Swedish" }, { L"SV" } },
{ { L"Turkish" }, { L"TR" } },
{ { L"?" }, { L"auto" } }
};
bool translateSelectedOnly = true, useRateLimiter = true, rateLimitSelected = true, useCache = true, useFilter = true;
int tokenCount = 10, rateLimitTimespan = 60000, maxSentenceSize = 1000;
enum KeyType { CAT, REST };
int keyType = REST;
std::pair<bool, std::wstring> Translate(const std::wstring& text, TranslationParam tlp)
{
if (!tlp.authKey.empty())
{
std::string translateFromComponent = tlp.translateFrom == L"?" ? "" : "&source_lang=" + WideStringToString(codes.at(tlp.translateFrom));
if (HttpRequest httpRequest{
L"Mozilla/5.0 Textractor",
tlp.authKey.find(L":fx") == std::string::npos ? L"api.deepl.com" : L"api-free.deepl.com",
L"POST",
keyType == CAT ? L"/v1/translate" : L"/v2/translate",
FormatString("text=%S&auth_key=%S&target_lang=%S", Escape(text), tlp.authKey, codes.at(tlp.translateTo)) + translateFromComponent,
L"Content-Type: application/x-www-form-urlencoded"
}; httpRequest && (httpRequest.response.find(L"translations") != std::string::npos || (httpRequest = HttpRequest{
L"Mozilla/5.0 Textractor",
tlp.authKey.find(L":fx") == std::string::npos ? L"api.deepl.com" : L"api-free.deepl.com",
L"POST",
(keyType = !keyType) == CAT ? L"/v1/translate" : L"/v2/translate",
FormatString("text=%S&auth_key=%S&target_lang=%S", Escape(text), tlp.authKey, codes.at(tlp.translateTo)) + translateFromComponent,
L"Content-Type: application/x-www-form-urlencoded"
})))
// Response formatted as JSON: translation starts with text":" and ends with "}]
if (auto translation = Copy(JSON::Parse(httpRequest.response)[L"translations"][0][L"text"].String())) return { true, translation.value() };
else return { false, FormatString(L"%s: %s", TRANSLATION_ERROR, httpRequest.response) };
else return { false, FormatString(L"%s (code=%u)", TRANSLATION_ERROR, httpRequest.errorCode) };
}
// the following code was reverse engineered from the DeepL website; it's as close as I could make it but I'm not sure what parts of this could be removed and still have it work
int id = 10000 * std::uniform_int_distribution(0, 9999)(std::random_device()) + 1;
int64_t r = _time64(nullptr), n = std::count(text.begin(), text.end(), L'i') + 1;
// user_preferred_langs? what should priority be? does timestamp do anything? other translation quality options?
auto body = FormatString(R"(
{
"id": %d,
"jsonrpc": "2.0",
"method": "LMT_handle_jobs",
"params": {
"priority": -1,
"timestamp": %lld,
"lang": {
"target_lang": "%.2S",
"source_lang_user_selected": "%S"
},
"jobs": [{
"raw_en_sentence": "%s",
"raw_en_context_before": [],
"kind": "default",
"preferred_num_beams": 1,
"quality": "fast",
"raw_en_context_after": []
}]
}
}
)", id, r + (n - r % n), codes.at(tlp.translateTo), codes.at(tlp.translateFrom), JSON::Escape(WideStringToString(text)));
// missing accept-encoding header since it fucks up HttpRequest
if (HttpRequest httpRequest{
L"Mozilla/5.0 Textractor",
L"www2.deepl.com",
L"POST",
L"/jsonrpc",
body,
L"Host: www2.deepl.com\r\nAccept-Language: en-US,en;q=0.5\r\nContent-type: application/json; charset=utf-8\r\nOrigin: https://www.deepl.com\r\nTE: Trailers",
INTERNET_DEFAULT_PORT,
L"https://www.deepl.com/translator",
WINHTTP_FLAG_SECURE
})
if (auto translation = Copy(JSON::Parse(httpRequest.response)[L"result"][L"translations"][0][L"beams"][0][L"postprocessed_sentence"].String())) return { true, translation.value() };
else return { false, FormatString(L"%s: %s", TRANSLATION_ERROR, httpRequest.response) };
else return { false, FormatString(L"%s (code=%u)", TRANSLATION_ERROR, httpRequest.errorCode) };
}

View File

@ -0,0 +1,173 @@
#include "devtools.h"
#include <ppltasks.h>
#include <ShlObj.h>
#include <QWebSocket>
#include <QMetaEnum>
#include <QFileDialog>
#include <QMouseEvent>
extern const char* CHROME_LOCATION;
extern const char* START_DEVTOOLS;
extern const char* STOP_DEVTOOLS;
extern const char* HIDE_CHROME;
extern const char* DEVTOOLS_STATUS;
extern const char* AUTO_START;
extern const char* TRANSLATION_PROVIDER;
extern QFormLayout* display;
extern Settings settings;
namespace
{
QLabel* statusLabel;
AutoHandle<> process = NULL;
QWebSocket webSocket;
std::atomic<int> idCounter = 0;
Synchronized<std::unordered_map<int, concurrency::task_completion_event<JSON::Value<wchar_t>>>> mapQueue;
void StatusChanged(QString status)
{
QMetaObject::invokeMethod(statusLabel, std::bind(&QLabel::setText, statusLabel, status));
}
void Start(std::wstring chromePath, bool headless)
{
if (process) DevTools::Close();
auto args = FormatString(
L"%s --proxy-server=direct:// --disable-extensions --disable-gpu --no-first-run --user-data-dir=\"%s\\devtoolscache\" --remote-debugging-port=9222",
chromePath,
std::filesystem::current_path().wstring()
);
args += headless ? L" --window-size=1920,1080 --headless" : L" --window-size=850,900";
DWORD exitCode = 0;
STARTUPINFOW DUMMY = { sizeof(DUMMY) };
PROCESS_INFORMATION processInfo = {};
if (!CreateProcessW(NULL, args.data(), nullptr, nullptr, FALSE, 0, nullptr, nullptr, &DUMMY, &processInfo)) return StatusChanged("StartupFailed");
CloseHandle(processInfo.hThread);
process = processInfo.hProcess;
if (HttpRequest httpRequest{
L"Mozilla/5.0 Textractor",
L"127.0.0.1",
L"POST",
L"/json/list",
"",
NULL,
9222,
NULL,
WINHTTP_FLAG_ESCAPE_DISABLE
})
if (auto list = Copy(JSON::Parse(httpRequest.response).Array())) if (auto it = std::find_if(
list->begin(),
list->end(),
[](const JSON::Value<wchar_t>& object) { return object[L"type"].String() && *object[L"type"].String() == L"page" && object[L"webSocketDebuggerUrl"].String(); }
); it != list->end()) return webSocket.open(S(*(*it)[L"webSocketDebuggerUrl"].String()));
StatusChanged("ConnectingFailed");
}
auto _ = ([]
{
QObject::connect(&webSocket, &QWebSocket::stateChanged,
[](QAbstractSocket::SocketState state) { StatusChanged(QMetaEnum::fromType<QAbstractSocket::SocketState>().valueToKey(state)); });
QObject::connect(&webSocket, &QWebSocket::textMessageReceived, [](QString message)
{
auto result = JSON::Parse(S(message));
auto mapQueue = ::mapQueue.Acquire();
if (auto id = result[L"id"].Number()) if (auto request = mapQueue->find((int)*id); request != mapQueue->end())
{
request->second.set(result);
mapQueue->erase(request);
}
});
}(), 0);
}
namespace DevTools
{
void Initialize()
{
QString chromePath = settings.value(CHROME_LOCATION).toString();
if (chromePath.isEmpty())
{
for (auto [_, process] : GetAllProcesses())
if (process && (process->find(L"\\chrome.exe") != std::string::npos || process->find(L"\\msedge.exe") != std::string::npos)) chromePath = S(process.value());
wchar_t programFiles[MAX_PATH + 100] = {};
for (auto folder : { CSIDL_PROGRAM_FILESX86, CSIDL_PROGRAM_FILES, CSIDL_LOCAL_APPDATA })
{
SHGetFolderPathW(NULL, folder, NULL, SHGFP_TYPE_CURRENT, programFiles);
wcscat_s(programFiles, L"/Google/Chrome/Application/chrome.exe");
if (std::filesystem::exists(programFiles)) chromePath = S(programFiles);
}
}
auto chromePathEdit = new QLineEdit(chromePath);
static struct : QObject
{
bool eventFilter(QObject* object, QEvent* event)
{
if (auto mouseEvent = dynamic_cast<QMouseEvent*>(event))
if (mouseEvent->button() == Qt::LeftButton)
if (QString chromePath = QFileDialog::getOpenFileName(nullptr, TRANSLATION_PROVIDER, "/", "Google Chrome (*.exe)"); !chromePath.isEmpty())
((QLineEdit*)object)->setText(chromePath);
return false;
}
} chromeSelector;
chromePathEdit->installEventFilter(&chromeSelector);
QObject::connect(chromePathEdit, &QLineEdit::textChanged, [chromePathEdit](QString path) { settings.setValue(CHROME_LOCATION, path); });
display->addRow(CHROME_LOCATION, chromePathEdit);
auto headlessCheck = new QCheckBox();
auto startButton = new QPushButton(START_DEVTOOLS), stopButton = new QPushButton(STOP_DEVTOOLS);
headlessCheck->setChecked(settings.value(HIDE_CHROME, true).toBool());
QObject::connect(headlessCheck, &QCheckBox::clicked, [](bool headless) { settings.setValue(HIDE_CHROME, headless); });
QObject::connect(startButton, &QPushButton::clicked, [chromePathEdit, headlessCheck] { Start(S(chromePathEdit->text()), headlessCheck->isChecked()); });
QObject::connect(stopButton, &QPushButton::clicked, &Close);
auto buttons = new QHBoxLayout();
buttons->addWidget(startButton);
buttons->addWidget(stopButton);
display->addRow(HIDE_CHROME, headlessCheck);
auto autoStartCheck = new QCheckBox();
autoStartCheck->setChecked(settings.value(AUTO_START, false).toBool());
QObject::connect(autoStartCheck, &QCheckBox::clicked, [](bool autoStart) { settings.setValue(AUTO_START, autoStart); });
display->addRow(AUTO_START, autoStartCheck);
display->addRow(buttons);
statusLabel = new QLabel("Stopped");
statusLabel->setFrameStyle(QFrame::Panel | QFrame::Sunken);
display->addRow(DEVTOOLS_STATUS, statusLabel);
if (autoStartCheck->isChecked()) QMetaObject::invokeMethod(startButton, &QPushButton::click, Qt::QueuedConnection);
}
void Close()
{
webSocket.close();
for (const auto& [_, task] : mapQueue.Acquire().contents) task.set_exception(std::runtime_error("closed"));
mapQueue->clear();
if (process)
{
TerminateProcess(process, 0);
WaitForSingleObject(process, 1000);
for (int retry = 0; ++retry < 20; Sleep(100))
try { std::filesystem::remove_all(L"devtoolscache"); break; }
catch (std::filesystem::filesystem_error) { continue; }
}
process = NULL;
StatusChanged("Stopped");
}
bool Connected()
{
return webSocket.state() == QAbstractSocket::ConnectedState;
}
JSON::Value<wchar_t> SendRequest(const char* method, const std::wstring& params)
{
concurrency::task_completion_event<JSON::Value<wchar_t>> response;
int id = idCounter += 1;
if (!Connected()) return {};
mapQueue->try_emplace(id, response);
QMetaObject::invokeMethod(&webSocket, std::bind(&QWebSocket::sendTextMessage, &webSocket, S(FormatString(LR"({"id":%d,"method":"%S","params":%s})", id, method, params))));
try { if (auto result = create_task(response).get()[L"result"]) return result; } catch (...) {}
return {};
}
}

View File

@ -0,0 +1,10 @@
#include "qtcommon.h"
#include "network.h"
namespace DevTools
{
void Initialize();
void Close();
bool Connected();
JSON::Value<wchar_t> SendRequest(const char* method, const std::wstring& params = L"{}");
}

View File

@ -0,0 +1,148 @@
#include "qtcommon.h"
#include "translatewrapper.h"
#include "devtools.h"
extern const wchar_t* ERROR_START_CHROME;
extern const wchar_t* TRANSLATION_ERROR;
const char* TRANSLATION_PROVIDER = "DevTools DeepL Translate";
const char* GET_API_KEY_FROM = nullptr;
extern const QStringList languagesTo
{
"Bulgarian",
"Chinese (Simplified)",
"Czech",
"Danish",
"Dutch",
"English (American)",
"English (British)",
"Estonian",
"Finnish",
"French",
"German",
"Greek",
"Hungarian",
"Italian",
"Japanese",
"Latvian",
"Lithuanian",
"Polish",
"Portuguese",
"Portuguese (Brazilian)",
"Romanian",
"Russian",
"Slovak",
"Slovenian",
"Spanish",
"Swedish"
},
languagesFrom =
{
"Bulgarian",
"Chinese",
"Czech",
"Danish",
"Dutch",
"English",
"Estonian",
"Finnish",
"French",
"German",
"Greek",
"Hungarian",
"Italian",
"Japanese",
"Latvian",
"Lithuanian",
"Polish",
"Portuguese",
"Romanian",
"Russian",
"Slovak",
"Slovenian",
"Spanish",
"Swedish"
};
extern const std::unordered_map<std::wstring, std::wstring> codes
{
{ { L"Bulgarian" }, { L"Bulgarian" } },
{ { L"Chinese" }, { L"Chinese" } },
{ { L"Chinese (Simplified)" }, { L"Chinese (simplified)" } },
{ { L"Czech" }, { L"Czech" } },
{ { L"Danish" }, { L"Danish" } },
{ { L"Dutch" }, { L"Dutch" } },
{ { L"English" }, { L"English" } },
{ { L"English (American)" }, { L"English (American)" } },
{ { L"English (British)" }, { L"English (British)" } },
{ { L"Estonian" }, { L"Estonian" } },
{ { L"Finnish" }, { L"Finnish" } },
{ { L"French" }, { L"French" } },
{ { L"German" }, { L"German" } },
{ { L"Greek" }, { L"Greek" } },
{ { L"Hungarian" }, { L"Hungarian" } },
{ { L"Italian" }, { L"Italian" } },
{ { L"Japanese" }, { L"Japanese" } },
{ { L"Latvian" }, { L"Latvian" } },
{ { L"Lithuanian" }, { L"Lithuanian" } },
{ { L"Polish" }, { L"Polish" } },
{ { L"Portuguese" }, { L"Portuguese" } },
{ { L"Portuguese (Brazilian)" }, { L"Portuguese (Brazilian)" } },
{ { L"Romanian" }, { L"Romanian" } },
{ { L"Russian" }, { L"Russian" } },
{ { L"Slovak" }, { L"Slovak" } },
{ { L"Slovenian" }, { L"Slovenian" } },
{ { L"Spanish" }, { L"Spanish" } },
{ { L"Swedish" }, { L"Swedish" } },
{ { L"?" }, { L"Detect language" } }
};
bool translateSelectedOnly = true, useRateLimiter = true, rateLimitSelected = false, useCache = true, useFilter = true;
int tokenCount = 30, rateLimitTimespan = 60000, maxSentenceSize = 2500;
BOOL WINAPI DllMain(HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved)
{
switch (ul_reason_for_call)
{
case DLL_PROCESS_ATTACH:
{
DevTools::Initialize();
}
break;
case DLL_PROCESS_DETACH:
{
DevTools::Close();
}
break;
}
return TRUE;
}
std::pair<bool, std::wstring> Translate(const std::wstring& text, TranslationParam tlp)
{
if (!DevTools::Connected()) return { false, FormatString(L"%s: %s", TRANSLATION_ERROR, ERROR_START_CHROME) };
// DevTools can't handle concurrent translations yet
static std::mutex translationMutex;
std::scoped_lock lock(translationMutex);
std::wstring escaped; // DeepL breaks with slash in input
for (auto ch : text) ch == '/' ? escaped += L"\\/" : escaped += ch;
DevTools::SendRequest("Page.navigate", FormatString(LR"({"url":"https://www.deepl.com/en/translator#en/en/%s"})", Escape(escaped)));
for (int retry = 0; ++retry < 20; Sleep(100))
if (Copy(DevTools::SendRequest("Runtime.evaluate", LR"({"expression":"document.readyState"})")[L"result"][L"value"].String()) == L"complete") break;
DevTools::SendRequest("Runtime.evaluate", FormatString(LR"({"expression":"
document.querySelector('.lmt__language_select--source').querySelector('button').click();
document.evaluate(`//*[text()='%s']`,document.querySelector('.lmt__language_select__menu'),null,XPathResult.FIRST_ORDERED_NODE_TYPE,null).singleNodeValue.click();
document.querySelector('.lmt__language_select--target').querySelector('button').click();
document.evaluate(`//*[text()='%s']`,document.querySelector('.lmt__language_select__menu'),null,XPathResult.FIRST_ORDERED_NODE_TYPE,null).singleNodeValue.click();
"})", codes.at(tlp.translateFrom), codes.at(tlp.translateTo)));
for (int retry = 0; ++retry < 100; Sleep(100))
if (auto translation = Copy(DevTools::SendRequest("Runtime.evaluate",
LR"({"expression":"document.querySelector('#target-dummydiv').innerHTML.trim() ","returnByValue":true})"
)[L"result"][L"value"].String())) if (!translation->empty()) return { true, translation.value() };
if (auto errorMessage = Copy(DevTools::SendRequest("Runtime.evaluate",
LR"({"expression":"document.querySelector('div.lmt__system_notification').innerHTML","returnByValue":true})"
)[L"result"][L"value"].String())) return { false, FormatString(L"%s: %s", TRANSLATION_ERROR, errorMessage.value()) };
return { false, TRANSLATION_ERROR };
}

View File

@ -0,0 +1,82 @@
#include "qtcommon.h"
#include "translatewrapper.h"
#include "devtools.h"
extern const wchar_t* ERROR_START_CHROME;
extern const wchar_t* TRANSLATION_ERROR;
const char* TRANSLATION_PROVIDER = "DevTools Papago Translate";
const char* GET_API_KEY_FROM = nullptr;
extern const QStringList languagesTo
{
"Chinese (Simplified)",
"Chinese (Traditional)",
"English",
"French",
"German",
"Hindi",
"Indonesian",
"Italian",
"Japanese",
"Korean",
"Portuguese",
"Russian",
"Spanish",
"Thai",
"Vietnamese",
}, languagesFrom = languagesTo;
extern const std::unordered_map<std::wstring, std::wstring> codes
{
{ { L"Chinese (Simplified)" }, { L"zh-CN" } },
{ { L"Chinese (Traditional)" }, { L"zt-TW" } },
{ { L"English" }, { L"en" } },
{ { L"French" }, { L"fr" } },
{ { L"German" }, { L"de" } },
{ { L"Hindi" }, { L"hi" } },
{ { L"Indonesian" }, { L"id" } },
{ { L"Italian" }, { L"it" } },
{ { L"Japanese" }, { L"ja" } },
{ { L"Korean" }, { L"ko" } },
{ { L"Portuguese" }, { L"pt" } },
{ { L"Russian" }, { L"ru" } },
{ { L"Spanish" }, { L"es" } },
{ { L"Thai" }, { L"th" } },
{ { L"Vietnamese" }, { L"vi" } },
{ { L"?" }, { L"auto" } }
};
bool translateSelectedOnly = true, useRateLimiter = true, rateLimitSelected = false, useCache = true, useFilter = true;
int tokenCount = 30, rateLimitTimespan = 60000, maxSentenceSize = 2500;
BOOL WINAPI DllMain(HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved)
{
switch (ul_reason_for_call)
{
case DLL_PROCESS_ATTACH:
{
DevTools::Initialize();
}
break;
case DLL_PROCESS_DETACH:
{
DevTools::Close();
}
break;
}
return TRUE;
}
std::pair<bool, std::wstring> Translate(const std::wstring& text, TranslationParam tlp)
{
if (!DevTools::Connected()) return { false, FormatString(L"%s: %s", TRANSLATION_ERROR, ERROR_START_CHROME) };
// DevTools can't handle concurrent translations yet
static std::mutex translationMutex;
std::scoped_lock lock(translationMutex);
DevTools::SendRequest("Page.navigate", FormatString(LR"({"url":"https://papago.naver.com/?sk=%s&tk=%s&st=%s"})", codes.at(tlp.translateFrom), codes.at(tlp.translateTo), Escape(text)));
for (int retry = 0; ++retry < 100; Sleep(100))
if (auto translation = Copy(DevTools::SendRequest("Runtime.evaluate",
LR"({"expression":"document.querySelector('#txtTarget').textContent.trim() ","returnByValue":true})"
)[L"result"][L"value"].String())) if (!translation->empty()) return { true, translation.value() };
return { false, TRANSLATION_ERROR };
}

View File

@ -0,0 +1,152 @@
#include "qtcommon.h"
#include "translatewrapper.h"
#include "devtools.h"
extern const wchar_t* ERROR_START_CHROME;
extern const wchar_t* TRANSLATION_ERROR;
const char* TRANSLATION_PROVIDER = "DevTools Systran Translate";
const char* GET_API_KEY_FROM = nullptr;
extern const QStringList languagesTo
{
"Albanian",
"Arabic",
"Bengali",
"Bulgarian",
"Burmese",
"Catalan",
"Chinese (Simplified)",
"Chinese (Traditional)",
"Croatian",
"Czech",
"Danish",
"Dutch",
"English",
"Estonian",
"Finnish",
"French",
"German",
"Greek",
"Hebrew",
"Hindi",
"Hungarian",
"Indonesian",
"Italian",
"Japanese",
"Korean",
"Latvian",
"Lithuanian",
"Malay",
"Norwegian",
"Pashto",
"Persian",
"Polish",
"Portuguese",
"Romanian",
"Russian",
"Serbian",
"Slovak",
"Slovenian",
"Somali",
"Spanish",
"Swedish",
"Tagalog",
"Tamil",
"Thai",
"Turkish",
"Ukrainian",
"Urdu",
"Vietnamese"
}, languagesFrom = languagesTo;
extern const std::unordered_map<std::wstring, std::wstring> codes
{
{ { L"Albanian" }, { L"sq" } },
{ { L"Arabic" }, { L"ar" } },
{ { L"Bengali" }, { L"bn" } },
{ { L"Bulgarian" }, { L"bg" } },
{ { L"Burmese" }, { L"my" } },
{ { L"Catalan" }, { L"ca" } },
{ { L"Chinese (Simplified)" }, { L"zh" } },
{ { L"Chinese (Traditional)" }, { L"zt" } },
{ { L"Croatian" }, { L"hr" } },
{ { L"Czech" }, { L"cs" } },
{ { L"Danish" }, { L"da" } },
{ { L"Dutch" }, { L"nl" } },
{ { L"English" }, { L"en" } },
{ { L"Estonian" }, { L"et" } },
{ { L"Finnish" }, { L"fi" } },
{ { L"French" }, { L"fr" } },
{ { L"German" }, { L"de" } },
{ { L"Greek" }, { L"el" } },
{ { L"Hebrew" }, { L"he" } },
{ { L"Hindi" }, { L"hi" } },
{ { L"Hungarian" }, { L"hu" } },
{ { L"Indonesian" }, { L"id" } },
{ { L"Italian" }, { L"it" } },
{ { L"Japanese" }, { L"ja" } },
{ { L"Korean" }, { L"ko" } },
{ { L"Latvian" }, { L"lv" } },
{ { L"Lithuanian" }, { L"lt" } },
{ { L"Malay" }, { L"ms" } },
{ { L"Norwegian" }, { L"no" } },
{ { L"Pashto" }, { L"ps" } },
{ { L"Persian" }, { L"fa" } },
{ { L"Polish" }, { L"pl" } },
{ { L"Portuguese" }, { L"pt" } },
{ { L"Romanian" }, { L"ro" } },
{ { L"Russian" }, { L"ru" } },
{ { L"Serbian" }, { L"sr" } },
{ { L"Slovak" }, { L"sk" } },
{ { L"Slovenian" }, { L"sl" } },
{ { L"Somali" }, { L"so" } },
{ { L"Spanish" }, { L"es" } },
{ { L"Swedish" }, { L"sv" } },
{ { L"Tagalog" }, { L"tl" } },
{ { L"Tamil" }, { L"ta" } },
{ { L"Thai" }, { L"th" } },
{ { L"Turkish" }, { L"tr" } },
{ { L"Ukrainian" }, { L"uk" } },
{ { L"Urdu" }, { L"ur" } },
{ { L"Vietnamese" }, { L"vi" } },
{ { L"?" }, { L"autodetect" } }
};
bool translateSelectedOnly = true, useRateLimiter = true, rateLimitSelected = false, useCache = true, useFilter = true;
int tokenCount = 30, rateLimitTimespan = 60000, maxSentenceSize = 2500;
BOOL WINAPI DllMain(HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved)
{
switch (ul_reason_for_call)
{
case DLL_PROCESS_ATTACH:
{
DevTools::Initialize();
}
break;
case DLL_PROCESS_DETACH:
{
DevTools::Close();
}
break;
}
return TRUE;
}
std::pair<bool, std::wstring> Translate(const std::wstring& text, TranslationParam tlp)
{
if (!DevTools::Connected()) return { false, FormatString(L"%s: %s", TRANSLATION_ERROR, ERROR_START_CHROME) };
// DevTools can't handle concurrent translations yet
static std::mutex translationMutex;
std::scoped_lock lock(translationMutex);
DevTools::SendRequest(
"Page.navigate",
FormatString(LR"({"url":"https://translate.systran.net/?source=%s&target=%s&input=%s"})", codes.at(tlp.translateFrom), codes.at(tlp.translateTo), Escape(text))
);
for (int retry = 0; ++retry < 100; Sleep(100))
if (auto translation = Copy(DevTools::SendRequest("Runtime.evaluate",
LR"({"expression":"document.querySelector('#outputEditor').textContent.trim() ","returnByValue":true})"
)[L"result"][L"value"].String())) if (!translation->empty()) return { true, translation.value() };
return { false, TRANSLATION_ERROR };
}

View File

@ -0,0 +1,191 @@
#define WIN32_LEAN_AND_MEAN
#include <Windows.h>
#include <concrt.h>
#include <string>
#include <vector>
#include <deque>
#include <array>
#include <map>
#include <unordered_map>
#include <set>
#include <unordered_set>
#include <Psapi.h>
#include <functional>
#include <algorithm>
#include <regex>
#include <memory>
#include <optional>
#include <thread>
#include <mutex>
#include <atomic>
#include <filesystem>
#include <iostream>
#include <sstream>
#include <locale>
#include <cstdint>
#include <list>
#include <utility>
#include <cassert>
#include <variant>
template <typename T, typename... Xs> struct ArrayImpl { using Type = std::tuple<T, Xs...>[]; };
template <typename T> struct ArrayImpl<T> { using Type = T[]; };
template <typename... Ts> using Array = typename ArrayImpl<Ts...>::Type;
template <auto F> using Functor = std::integral_constant<std::remove_reference_t<decltype(F)>, F>; // shouldn't need remove_reference_t but MSVC is bugged
struct PermissivePointer
{
template <typename T> operator T*() { return (T*)p; }
void* p;
};
template <typename HandleCloser = Functor<CloseHandle>>
class AutoHandle
{
public:
AutoHandle(HANDLE h) : h(h) {}
operator HANDLE() { return h.get(); }
PHANDLE operator&() { static_assert(sizeof(*this) == sizeof(HANDLE)); assert(!h); return (PHANDLE)this; }
operator bool() { return h.get() != NULL && h.get() != INVALID_HANDLE_VALUE; }
private:
struct HandleCleaner { void operator()(void* h) { if (h != INVALID_HANDLE_VALUE) HandleCloser()(PermissivePointer{ h }); } };
std::unique_ptr<void, HandleCleaner> h;
};
template<typename T, typename M = std::mutex>
class Synchronized
{
public:
template <typename... Args>
Synchronized(Args&&... args) : contents(std::forward<Args>(args)...) {}
struct Locker
{
T* operator->() { return &contents; }
std::unique_lock<M> lock;
T& contents;
};
Locker Acquire() { return { std::unique_lock(m), contents }; }
Locker operator->() { return Acquire(); }
T Copy() { return Acquire().contents; }
private:
T contents;
M m;
};
template <typename F>
void SpawnThread(const F& f) // works in DllMain unlike std thread
{
F* copy = new F(f);
CloseHandle(CreateThread(nullptr, 0, [](void* copy)
{
(*(F*)copy)();
delete (F*)copy;
return 0UL;
}, copy, 0, nullptr));
}
inline struct
{
inline static BYTE DUMMY[100];
template <typename T> operator T*() { static_assert(sizeof(T) < sizeof(DUMMY)); return (T*)DUMMY; }
} DUMMY;
inline auto Swallow = [](auto&&...) {};
template <typename T> std::optional<std::remove_cv_t<T>> Copy(T* ptr) { if (ptr) return *ptr; return {}; }
template <typename T> inline auto FormatArg(T arg) { return arg; }
template <typename C> inline auto FormatArg(const std::basic_string<C>& arg) { return arg.c_str(); }
#pragma warning(push)
#pragma warning(disable: 4996)
template <typename... Args>
inline std::string FormatString(const char* format, const Args&... args)
{
std::string buffer(snprintf(nullptr, 0, format, FormatArg(args)...), '\0');
sprintf(buffer.data(), format, FormatArg(args)...);
return buffer;
}
template <typename... Args>
inline std::wstring FormatString(const wchar_t* format, const Args&... args)
{
std::wstring buffer(_snwprintf(nullptr, 0, format, FormatArg(args)...), L'\0');
_swprintf(buffer.data(), format, FormatArg(args)...);
return buffer;
}
#pragma warning(pop)
inline void Trim(std::wstring& text)
{
text.erase(text.begin(), std::find_if_not(text.begin(), text.end(), iswspace));
text.erase(std::find_if_not(text.rbegin(), text.rend(), iswspace).base(), text.end());
}
inline std::optional<std::wstring> StringToWideString(const std::string& text, UINT encoding)
{
std::vector<wchar_t> buffer(text.size() + 1);
if (int length = MultiByteToWideChar(encoding, 0, text.c_str(), text.size() + 1, buffer.data(), buffer.size()))
return std::wstring(buffer.data(), length - 1);
return {};
}
inline std::wstring StringToWideString(const std::string& text)
{
std::vector<wchar_t> buffer(text.size() + 1);
MultiByteToWideChar(CP_UTF8, 0, text.c_str(), -1, buffer.data(), buffer.size());
return buffer.data();
}
inline std::string WideStringToString(const std::wstring& text)
{
std::vector<char> buffer((text.size() + 1) * 4);
WideCharToMultiByte(CP_UTF8, 0, text.c_str(), -1, buffer.data(), buffer.size(), nullptr, nullptr);
return buffer.data();
}
template <typename... Args>
inline void TEXTRACTOR_MESSAGE(const wchar_t* format, const Args&... args) { MessageBoxW(NULL, FormatString(format, args...).c_str(), L"Textractor", MB_OK); }
template <typename... Args>
inline void TEXTRACTOR_DEBUG(const wchar_t* format, const Args&... args) { SpawnThread([=] { TEXTRACTOR_MESSAGE(format, args...); }); }
void Localize();
#ifdef _DEBUG
#define TEST(...) static auto _ = CreateThread(nullptr, 0, [](auto) { __VA_ARGS__; return 0UL; }, NULL, 0, nullptr)
#else
#define TEST(...)
#endif
inline std::optional<std::wstring> GetModuleFilename(DWORD processId, HMODULE module = NULL)
{
std::vector<wchar_t> buffer(MAX_PATH);
if (AutoHandle<> process = OpenProcess(PROCESS_VM_READ | PROCESS_QUERY_INFORMATION, FALSE, processId))
if (GetModuleFileNameExW(process, module, buffer.data(), MAX_PATH)) return buffer.data();
return {};
}
inline std::optional<std::wstring> GetModuleFilename(HMODULE module = NULL)
{
std::vector<wchar_t> buffer(MAX_PATH);
if (GetModuleFileNameW(module, buffer.data(), MAX_PATH)) return buffer.data();
return {};
}
inline std::vector<std::pair<DWORD, std::optional<std::wstring>>> GetAllProcesses()
{
std::vector<DWORD> processIds(10000);
DWORD spaceUsed = 0;
EnumProcesses(processIds.data(), 10000 * sizeof(DWORD), &spaceUsed);
std::vector<std::pair<DWORD, std::optional<std::wstring>>> processes;
for (int i = 0; i < spaceUsed / sizeof(DWORD); ++i) processes.push_back({ processIds[i], GetModuleFilename(processIds[i]) });
return processes;
}

View File

@ -0,0 +1,8 @@
#include "extension.h"
bool ProcessSentence(std::wstring& sentence, SentenceInfo sentenceInfo)
{
if (sentenceInfo["text number"] == 0) return false;
sentence += L"\n";
return true;
}

View File

@ -0,0 +1,606 @@
#include "qtcommon.h"
#include "extension.h"
#include "ui_extrawindow.h"
#include "blockmarkup.h"
#include <fstream>
#include <process.h>
#include <QRegularExpression>
#include <QColorDialog>
#include <QFontDialog>
#include <QMenu>
#include <QPainter>
#include <QGraphicsEffect>
#include <QFontMetrics>
#include <QMouseEvent>
#include <QWheelEvent>
#include <QScrollArea>
#include <QAbstractNativeEventFilter>
extern const char* EXTRA_WINDOW_INFO;
extern const char* TOPMOST;
extern const char* OPACITY;
extern const char* SHOW_ORIGINAL;
extern const char* ORIGINAL_AFTER_TRANSLATION;
extern const char* SIZE_LOCK;
extern const char* POSITION_LOCK;
extern const char* CENTERED_TEXT;
extern const char* AUTO_RESIZE_WINDOW_HEIGHT;
extern const char* CLICK_THROUGH;
extern const char* HIDE_MOUSEOVER;
extern const char* DICTIONARY;
extern const char* DICTIONARY_INSTRUCTIONS;
extern const char* BG_COLOR;
extern const char* TEXT_COLOR;
extern const char* TEXT_OUTLINE;
extern const char* OUTLINE_COLOR;
extern const char* OUTLINE_SIZE;
extern const char* OUTLINE_SIZE_INFO;
extern const char* FONT;
constexpr auto DICTIONARY_SAVE_FILE = u8"SavedDictionary.txt";
constexpr int CLICK_THROUGH_HOTKEY = 0xc0d0;
QColor colorPrompt(QWidget* parent, QColor default, const QString& title, bool customOpacity = true)
{
QColor color = QColorDialog::getColor(default, parent, title);
if (customOpacity) color.setAlpha(255 * QInputDialog::getDouble(parent, title, OPACITY, default.alpha() / 255.0, 0, 1, 3, nullptr, Qt::WindowCloseButtonHint));
return color;
}
struct PrettyWindow : QDialog, Localizer
{
PrettyWindow(const char* name)
{
ui.setupUi(this);
ui.display->setGraphicsEffect(outliner = new Outliner);
setWindowFlags(Qt::FramelessWindowHint);
setAttribute(Qt::WA_TranslucentBackground);
settings.beginGroup(name);
QFont font = ui.display->font();
if (font.fromString(settings.value(FONT, font.toString()).toString())) ui.display->setFont(font);
SetBackgroundColor(settings.value(BG_COLOR, backgroundColor).value<QColor>());
SetTextColor(settings.value(TEXT_COLOR, TextColor()).value<QColor>());
outliner->color = settings.value(OUTLINE_COLOR, outliner->color).value<QColor>();
outliner->size = settings.value(OUTLINE_SIZE, outliner->size).toDouble();
autoHide = settings.value(HIDE_MOUSEOVER, autoHide).toBool();
menu.addAction(FONT, this, &PrettyWindow::RequestFont);
menu.addAction(BG_COLOR, [this] { SetBackgroundColor(colorPrompt(this, backgroundColor, BG_COLOR)); });
menu.addAction(TEXT_COLOR, [this] { SetTextColor(colorPrompt(this, TextColor(), TEXT_COLOR)); });
QAction* outlineAction = menu.addAction(TEXT_OUTLINE, this, &PrettyWindow::SetOutline);
outlineAction->setCheckable(true);
outlineAction->setChecked(outliner->size >= 0);
QAction* autoHideAction = menu.addAction(HIDE_MOUSEOVER, this, [this](bool autoHide) { settings.setValue(HIDE_MOUSEOVER, this->autoHide = autoHide); });
autoHideAction->setCheckable(true);
autoHideAction->setChecked(autoHide);
connect(this, &QDialog::customContextMenuRequested, [this](QPoint point) { menu.exec(mapToGlobal(point)); });
connect(ui.display, &QLabel::customContextMenuRequested, [this](QPoint point) { menu.exec(ui.display->mapToGlobal(point)); });
startTimer(50);
}
~PrettyWindow()
{
settings.sync();
}
Ui::ExtraWindow ui;
protected:
void timerEvent(QTimerEvent*) override
{
if (autoHide && geometry().contains(QCursor::pos()))
{
if (!hidden)
{
if (backgroundColor.alphaF() > 0.05) backgroundColor.setAlphaF(0.05);
if (outliner->color.alphaF() > 0.05) outliner->color.setAlphaF(0.05);
QColor hiddenTextColor = TextColor();
if (hiddenTextColor.alphaF() > 0.05) hiddenTextColor.setAlphaF(0.05);
ui.display->setPalette(QPalette(hiddenTextColor, {}, {}, {}, {}, {}, {}));
hidden = true;
repaint();
}
}
else if (hidden)
{
backgroundColor.setAlpha(settings.value(BG_COLOR).value<QColor>().alpha());
outliner->color.setAlpha(settings.value(OUTLINE_COLOR).value<QColor>().alpha());
ui.display->setPalette(QPalette(settings.value(TEXT_COLOR).value<QColor>(), {}, {}, {}, {}, {}, {}));
hidden = false;
repaint();
}
}
QMenu menu{ ui.display };
Settings settings{ this };
private:
void RequestFont()
{
if (QFont font = QFontDialog::getFont(&ok, ui.display->font(), this, FONT); ok)
{
settings.setValue(FONT, font.toString());
ui.display->setFont(font);
}
};
void SetBackgroundColor(QColor color)
{
if (!color.isValid()) return;
if (color.alpha() == 0) color.setAlpha(1);
backgroundColor = color;
repaint();
settings.setValue(BG_COLOR, color.name(QColor::HexArgb));
};
QColor TextColor()
{
return ui.display->palette().color(QPalette::WindowText);
}
void SetTextColor(QColor color)
{
if (!color.isValid()) return;
ui.display->setPalette(QPalette(color, {}, {}, {}, {}, {}, {}));
settings.setValue(TEXT_COLOR, color.name(QColor::HexArgb));
};
void SetOutline(bool enable)
{
if (enable)
{
QColor color = colorPrompt(this, outliner->color, OUTLINE_COLOR);
if (color.isValid()) outliner->color = color;
outliner->size = QInputDialog::getDouble(this, OUTLINE_SIZE, OUTLINE_SIZE_INFO, -outliner->size, 0, INT_MAX, 2, nullptr, Qt::WindowCloseButtonHint);
}
else outliner->size = -outliner->size;
settings.setValue(OUTLINE_COLOR, outliner->color.name(QColor::HexArgb));
settings.setValue(OUTLINE_SIZE, outliner->size);
}
void paintEvent(QPaintEvent*) override
{
QPainter(this).fillRect(rect(), backgroundColor);
}
bool autoHide = false, hidden = false;
QColor backgroundColor{ palette().window().color() };
struct Outliner : QGraphicsEffect
{
void draw(QPainter* painter) override
{
if (size < 0) return drawSource(painter);
QPoint offset;
QPixmap pixmap = sourcePixmap(Qt::LogicalCoordinates, &offset);
offset.setX(offset.x() + size);
for (auto offset2 : Array<QPointF>{ { 0, 1 }, { 0, -1 }, { 1, 0 }, { -1, 0 }, { 1, 1 }, { 1, -1 }, { -1, 1 }, { -1, -1 } })
{
QImage outline = pixmap.toImage();
QPainter outlinePainter(&outline);
outlinePainter.setCompositionMode(QPainter::CompositionMode_SourceIn);
outlinePainter.fillRect(outline.rect(), color);
painter->drawImage(offset + offset2 * size, outline);
}
painter->drawPixmap(offset, pixmap);
}
QColor color{ Qt::black };
double size = -0.5;
}* outliner;
};
class ExtraWindow : public PrettyWindow, QAbstractNativeEventFilter
{
public:
ExtraWindow() : PrettyWindow("Extra Window")
{
ui.display->setTextFormat(Qt::PlainText);
if (settings.contains(WINDOW) && QApplication::screenAt(settings.value(WINDOW).toRect().bottomRight())) setGeometry(settings.value(WINDOW).toRect());
for (auto [name, default, slot] : Array<const char*, bool, void(ExtraWindow::*)(bool)>{
{ TOPMOST, false, &ExtraWindow::SetTopmost },
{ SIZE_LOCK, false, &ExtraWindow::SetSizeLock },
{ POSITION_LOCK, false, &ExtraWindow::SetPositionLock },
{ CENTERED_TEXT, false, &ExtraWindow::SetCenteredText },
{ AUTO_RESIZE_WINDOW_HEIGHT, false, &ExtraWindow::SetAutoResize },
{ SHOW_ORIGINAL, true, &ExtraWindow::SetShowOriginal },
{ ORIGINAL_AFTER_TRANSLATION, true, &ExtraWindow::SetShowOriginalAfterTranslation },
{ DICTIONARY, false, &ExtraWindow::SetUseDictionary },
})
{
// delay processing anything until Textractor has finished initializing
QMetaObject::invokeMethod(this, std::bind(slot, this, default = settings.value(name, default).toBool()), Qt::QueuedConnection);
auto action = menu.addAction(name, this, slot);
action->setCheckable(true);
action->setChecked(default);
}
menu.addAction(CLICK_THROUGH, this, &ExtraWindow::ToggleClickThrough);
ui.display->installEventFilter(this);
qApp->installNativeEventFilter(this);
QMetaObject::invokeMethod(this, [this]
{
RegisterHotKey((HWND)winId(), CLICK_THROUGH_HOTKEY, MOD_ALT | MOD_NOREPEAT, 0x58);
show();
AddSentence(EXTRA_WINDOW_INFO);
}, Qt::QueuedConnection);
}
~ExtraWindow()
{
settings.setValue(WINDOW, geometry());
}
void AddSentence(QString sentence)
{
sanitize(sentence);
sentence.chop(std::distance(std::remove(sentence.begin(), sentence.end(), QChar::Tabulation), sentence.end()));
sentenceHistory.push_back(sentence);
if (sentenceHistory.size() > 1000) sentenceHistory.erase(sentenceHistory.begin());
historyIndex = sentenceHistory.size() - 1;
DisplaySentence();
}
private:
void DisplaySentence()
{
if (sentenceHistory.empty()) return;
QString sentence = sentenceHistory[historyIndex];
if (sentence.contains(u8"\x200b \n"))
if (!showOriginal) sentence = sentence.split(u8"\x200b \n")[1];
else if (showOriginalAfterTranslation) sentence = sentence.split(u8"\x200b \n")[1] + "\n" + sentence.split(u8"\x200b \n")[0];
if (sizeLock && !autoResize)
{
QFontMetrics fontMetrics(ui.display->font(), ui.display);
int low = 0, high = sentence.size(), last = 0;
while (low <= high)
{
int mid = (low + high) / 2;
if (fontMetrics.boundingRect(0, 0, ui.display->width(), INT_MAX, Qt::TextWordWrap, sentence.left(mid)).height() <= ui.display->height())
{
last = mid;
low = mid + 1;
}
else high = mid - 1;
}
sentence = sentence.left(last);
}
ui.display->setText(sentence);
if (autoResize)
resize(width(), height() - ui.display->height() +
QFontMetrics(ui.display->font(), ui.display).boundingRect(0, 0, ui.display->width(), INT_MAX, Qt::TextWordWrap, sentence).height()
);
}
void SetTopmost(bool topmost)
{
for (auto window : { winId(), dictionaryWindow.winId() })
SetWindowPos((HWND)window, topmost ? HWND_TOPMOST : HWND_NOTOPMOST, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE);
settings.setValue(TOPMOST, topmost);
};
void SetPositionLock(bool locked)
{
settings.setValue(POSITION_LOCK, posLock = locked);
};
void SetSizeLock(bool locked)
{
setSizeGripEnabled(!locked);
settings.setValue(SIZE_LOCK, sizeLock = locked);
};
void SetCenteredText(bool centeredText)
{
ui.display->setAlignment(centeredText ? Qt::AlignHCenter : Qt::AlignLeft);
settings.setValue(CENTERED_TEXT, this->centeredText = centeredText);
};
void SetAutoResize(bool autoResize)
{
settings.setValue(AUTO_RESIZE_WINDOW_HEIGHT, this->autoResize = autoResize);
DisplaySentence();
};
void SetShowOriginal(bool showOriginal)
{
settings.setValue(SHOW_ORIGINAL, this->showOriginal = showOriginal);
DisplaySentence();
};
void SetShowOriginalAfterTranslation(bool showOriginalAfterTranslation)
{
settings.setValue(ORIGINAL_AFTER_TRANSLATION, this->showOriginalAfterTranslation = showOriginalAfterTranslation);
DisplaySentence();
};
void SetUseDictionary(bool useDictionary)
{
if (useDictionary)
{
dictionaryWindow.UpdateDictionary();
if (dictionaryWindow.dictionary.empty())
{
std::ofstream(DICTIONARY_SAVE_FILE) << u8"\ufeff" << DICTIONARY_INSTRUCTIONS;
_spawnlp(_P_DETACH, "notepad", "notepad", DICTIONARY_SAVE_FILE, NULL); // show file to user
}
}
settings.setValue(DICTIONARY, this->useDictionary = useDictionary);
}
void ToggleClickThrough()
{
clickThrough = !clickThrough;
for (auto window : { winId(), dictionaryWindow.winId() })
{
unsigned exStyle = GetWindowLongPtrW((HWND)window, GWL_EXSTYLE);
if (clickThrough) exStyle |= WS_EX_TRANSPARENT;
else exStyle &= ~WS_EX_TRANSPARENT;
SetWindowLongPtrW((HWND)window, GWL_EXSTYLE, exStyle);
}
};
void ShowDictionary(QPoint mouse)
{
QString sentence = ui.display->text();
const QFont& font = ui.display->font();
if (cachedDisplayInfo.CompareExchange(ui.display))
{
QFontMetrics fontMetrics(font, ui.display);
int flags = Qt::TextWordWrap | (ui.display->alignment() & (Qt::AlignLeft | Qt::AlignHCenter));
textPositionMap.clear();
for (int i = 0, height = 0, lineBreak = 0; i < sentence.size(); ++i)
{
int block = 1;
for (int charHeight = fontMetrics.boundingRect(0, 0, 1, INT_MAX, flags, sentence.mid(i, 1)).height();
i + block < sentence.size() && fontMetrics.boundingRect(0, 0, 1, INT_MAX, flags, sentence.mid(i, block + 1)).height() < charHeight * 1.5; ++block);
auto boundingRect = fontMetrics.boundingRect(0, 0, ui.display->width(), INT_MAX, flags, sentence.left(i + block));
if (boundingRect.height() > height)
{
height = boundingRect.height();
lineBreak = i;
}
textPositionMap.push_back({
fontMetrics.boundingRect(0, 0, ui.display->width(), INT_MAX, flags, sentence.mid(lineBreak, i - lineBreak + 1)).right() + 1,
height
});
}
}
int i;
for (i = 0; i < textPositionMap.size(); ++i) if (textPositionMap[i].y() > mouse.y() && textPositionMap[i].x() > mouse.x()) break;
if (i == textPositionMap.size() || (mouse - textPositionMap[i]).manhattanLength() > font.pointSize() * 3) return dictionaryWindow.hide();
if (sentence.mid(i) == dictionaryWindow.term) return dictionaryWindow.ShowDefinition();
dictionaryWindow.ui.display->setFixedWidth(ui.display->width() * 3 / 4);
dictionaryWindow.SetTerm(sentence.mid(i));
int left = i == 0 ? 0 : textPositionMap[i - 1].x(), right = textPositionMap[i].x(),
x = textPositionMap[i].x() > ui.display->width() / 2 ? -dictionaryWindow.width() + (right * 3 + left) / 4 : (left * 3 + right) / 4, y = 0;
for (auto point : textPositionMap) if (point.y() > y && point.y() < textPositionMap[i].y()) y = point.y();
dictionaryWindow.move(ui.display->mapToGlobal(QPoint(x, y - dictionaryWindow.height())));
}
bool nativeEventFilter(const QByteArray&, void* message, long* result) override
{
auto msg = (MSG*)message;
if (msg->message == WM_HOTKEY)
if (msg->wParam == CLICK_THROUGH_HOTKEY) return ToggleClickThrough(), true;
return false;
}
bool eventFilter(QObject*, QEvent* event) override
{
if (event->type() == QEvent::MouseButtonPress) mousePressEvent((QMouseEvent*)event);
return false;
}
void timerEvent(QTimerEvent* event) override
{
if (useDictionary && QCursor::pos() != oldPos && (!dictionaryWindow.isVisible() || !dictionaryWindow.geometry().contains(QCursor::pos())))
ShowDictionary(ui.display->mapFromGlobal(QCursor::pos()));
PrettyWindow::timerEvent(event);
}
void mousePressEvent(QMouseEvent* event) override
{
dictionaryWindow.hide();
oldPos = event->globalPos();
}
void mouseMoveEvent(QMouseEvent* event) override
{
if (!posLock) move(pos() + event->globalPos() - oldPos);
oldPos = event->globalPos();
}
void wheelEvent(QWheelEvent* event) override
{
int scroll = event->angleDelta().y();
if (scroll > 0 && historyIndex > 0) --historyIndex;
if (scroll < 0 && historyIndex + 1 < sentenceHistory.size()) ++historyIndex;
DisplaySentence();
}
bool sizeLock, posLock, centeredText, autoResize, showOriginal, showOriginalAfterTranslation, useDictionary, clickThrough;
QPoint oldPos;
class
{
public:
bool CompareExchange(QLabel* display)
{
if (display->text() == text && display->font() == font && display->width() == width && display->alignment() == alignment) return false;
text = display->text();
font = display->font();
width = display->width();
alignment = display->alignment();
return true;
}
private:
QString text;
QFont font;
int width;
Qt::Alignment alignment;
} cachedDisplayInfo;
std::vector<QPoint> textPositionMap;
std::vector<QString> sentenceHistory;
int historyIndex = 0;
class DictionaryWindow : public PrettyWindow
{
public:
DictionaryWindow() : PrettyWindow("Dictionary Window")
{
ui.display->setSizePolicy({ QSizePolicy::Fixed, QSizePolicy::Minimum });
}
void UpdateDictionary()
{
try
{
if (dictionaryFileLastWrite == std::filesystem::last_write_time(DICTIONARY_SAVE_FILE)) return;
dictionaryFileLastWrite = std::filesystem::last_write_time(DICTIONARY_SAVE_FILE);
}
catch (std::filesystem::filesystem_error) { return; }
dictionary.clear();
charStorage.clear();
auto StoreCopy = [&](std::string_view string)
{
auto location = &*charStorage.insert(charStorage.end(), string.begin(), string.end());
charStorage.push_back(0);
return location;
};
charStorage.reserve(std::filesystem::file_size(DICTIONARY_SAVE_FILE));
std::ifstream stream(DICTIONARY_SAVE_FILE);
BlockMarkupIterator savedDictionary(stream, Array<std::string_view>{ "|TERM|", "|DEFINITION|" });
while (auto read = savedDictionary.Next())
{
const auto& [terms, definition] = read.value();
auto storedDefinition = StoreCopy(definition);
std::string_view termsView = terms;
size_t start = 0, end = termsView.find("|TERM|");
while (end != std::string::npos)
{
dictionary.push_back(DictionaryEntry{ StoreCopy(termsView.substr(start, end - start)), storedDefinition });
start = end + 6;
end = termsView.find("|TERM|", start);
}
dictionary.push_back(DictionaryEntry{ StoreCopy(termsView.substr(start)), storedDefinition });
}
std::stable_sort(dictionary.begin(), dictionary.end());
inflections.clear();
stream.seekg(0);
BlockMarkupIterator savedInflections(stream, Array<std::string_view>{ "|ROOT|", "|INFLECTS TO|", "|NAME|" });
while (auto read = savedInflections.Next())
{
const auto& [root, inflectsTo, name] = read.value();
if (!inflections.emplace_back(Inflection{
S(root),
QRegularExpression(QRegularExpression::anchoredPattern(S(inflectsTo)), QRegularExpression::UseUnicodePropertiesOption),
S(name)
}).inflectsTo.isValid()) TEXTRACTOR_MESSAGE(L"Invalid regex: %s", StringToWideString(inflectsTo));
}
}
void SetTerm(QString term)
{
this->term = term;
UpdateDictionary();
definitions.clear();
definitionIndex = 0;
std::unordered_set<const char*> foundDefinitions;
for (term = term.left(100); !term.isEmpty(); term.chop(1))
for (const auto& [rootTerm, definition, inflections] : LookupDefinitions(term, foundDefinitions))
definitions.push_back(
QStringLiteral("<h3>%1 (%5/%6)</h3><small>%2%3</small>%4").arg(
term.split("<<")[0].toHtmlEscaped(),
rootTerm.split("<<")[0].toHtmlEscaped(),
inflections.join(""),
definition
)
);
for (int i = 0; i < definitions.size(); ++i) definitions[i] = definitions[i].arg(i + 1).arg(definitions.size());
ShowDefinition();
}
void ShowDefinition()
{
if (definitions.empty()) return hide();
ui.display->setText(definitions[definitionIndex]);
adjustSize();
resize(width(), 1);
show();
}
struct DictionaryEntry
{
const char* term;
const char* definition;
bool operator<(DictionaryEntry other) const { return strcmp(term, other.term) < 0; }
};
std::vector<DictionaryEntry> dictionary;
QString term;
private:
struct LookupResult
{
QString term;
QString definition;
QStringList inflectionsUsed;
};
std::vector<LookupResult> LookupDefinitions(QString term, std::unordered_set<const char*>& foundDefinitions, QStringList inflectionsUsed = {})
{
std::vector<LookupResult> results;
for (auto [it, end] = std::equal_range(dictionary.begin(), dictionary.end(), DictionaryEntry{ term.toUtf8() }); it != end; ++it)
if (foundDefinitions.emplace(it->definition).second) results.push_back({ term, it->definition, inflectionsUsed });
for (const auto& inflection : inflections) if (auto match = inflection.inflectsTo.match(term); match.hasMatch())
{
QStringList currentInflectionsUsed = inflectionsUsed;
currentInflectionsUsed.push_front(inflection.name);
QString root;
for (const auto& ch : inflection.root) root += ch.isDigit() ? match.captured(ch.digitValue()) : ch;
for (const auto& definition : LookupDefinitions(root, foundDefinitions, currentInflectionsUsed)) results.push_back(definition);
}
return results;
}
void wheelEvent(QWheelEvent* event) override
{
int scroll = event->angleDelta().y();
if (scroll > 0 && definitionIndex > 0) definitionIndex -= 1;
if (scroll < 0 && definitionIndex + 1 < definitions.size()) definitionIndex += 1;
int oldHeight = height();
ShowDefinition();
move(x(), y() + oldHeight - height());
}
struct Inflection
{
QString root;
QRegularExpression inflectsTo;
QString name;
};
std::vector<Inflection> inflections;
std::filesystem::file_time_type dictionaryFileLastWrite;
std::vector<char> charStorage;
std::vector<QString> definitions;
int definitionIndex;
} dictionaryWindow;
} extraWindow;
#include<stdio.h>
bool ProcessSentence(std::wstring& sentence, SentenceInfo sentenceInfo)
{
wprintf(L"enter %s\n",sentence.c_str());
if (sentenceInfo["current select"] && sentenceInfo["text number"] != 0)
QMetaObject::invokeMethod(&extraWindow, [sentence = S(sentence)] { extraWindow.AddSentence(sentence); });
wprintf(L"leave %s\n",sentence.c_str());
return false;
}

View File

@ -0,0 +1,51 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>ExtraWindow</class>
<widget class="QDialog" name="ExtraWindow">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>800</width>
<height>300</height>
</rect>
</property>
<property name="contextMenuPolicy">
<enum>Qt::CustomContextMenu</enum>
</property>
<layout class="QHBoxLayout">
<item>
<widget class="QLabel" name="display">
<property name="sizePolicy">
<sizepolicy hsizetype="Ignored" vsizetype="Ignored">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="font">
<font>
<pointsize>16</pointsize>
</font>
</property>
<property name="contextMenuPolicy">
<enum>Qt::CustomContextMenu</enum>
</property>
<property name="lineWidth">
<number>0</number>
</property>
<property name="alignment">
<set>Qt::AlignTop</set>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
<property name="textInteractionFlags">
<set>Qt::TextSelectableByMouse</set>
</property>
</widget>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>

View File

@ -0,0 +1,267 @@
#include "qtcommon.h"
#include "translatewrapper.h"
#include "network.h"
extern const wchar_t* TRANSLATION_ERROR;
const char* TRANSLATION_PROVIDER = "Google Translate";
const char* GET_API_KEY_FROM = "https://console.cloud.google.com/marketplace/product/google/translate.googleapis.com";
extern const QStringList languagesTo
{
"Afrikaans",
"Albanian",
"Amharic",
"Arabic",
"Armenian",
"Azerbaijani",
"Basque",
"Belarusian",
"Bengali",
"Bosnian",
"Bulgarian",
"Catalan",
"Cebuano",
"Chichewa",
"Chinese (Simplified)",
"Chinese (Traditional)",
"Corsican",
"Croatian",
"Czech",
"Danish",
"Dutch",
"English",
"Esperanto",
"Estonian",
"Filipino",
"Finnish",
"French",
"Frisian",
"Galician",
"Georgian",
"German",
"Greek",
"Gujarati",
"Haitian Creole",
"Hausa",
"Hawaiian",
"Hebrew",
"Hindi",
"Hmong",
"Hungarian",
"Icelandic",
"Igbo",
"Indonesian",
"Irish",
"Italian",
"Japanese",
"Javanese",
"Kannada",
"Kazakh",
"Khmer",
"Kinyarwanda",
"Korean",
"Kurdish (Kurmanji)",
"Kyrgyz",
"Lao",
"Latin",
"Latvian",
"Lithuanian",
"Luxembourgish",
"Macedonian",
"Malagasy",
"Malay",
"Malayalam",
"Maltese",
"Maori",
"Marathi",
"Mongolian",
"Myanmar (Burmese)",
"Nepali",
"Norwegian",
"Odia (Oriya)",
"Pashto",
"Persian",
"Polish",
"Portuguese",
"Punjabi",
"Romanian",
"Russian",
"Samoan",
"Scots Gaelic",
"Serbian",
"Sesotho",
"Shona",
"Sindhi",
"Sinhala",
"Slovak",
"Slovenian",
"Somali",
"Spanish",
"Sundanese",
"Swahili",
"Swedish",
"Tajik",
"Tamil",
"Tatar",
"Telugu",
"Thai",
"Turkish",
"Turkmen",
"Ukrainian",
"Urdu",
"Uyghur",
"Uzbek",
"Vietnamese",
"Welsh",
"Xhosa",
"Yiddish",
"Yoruba",
"Zulu",
}, languagesFrom = languagesTo;
extern const std::unordered_map<std::wstring, std::wstring> codes
{
{ { L"Afrikaans" }, { L"af" } },
{ { L"Albanian" }, { L"sq" } },
{ { L"Amharic" }, { L"am" } },
{ { L"Arabic" }, { L"ar" } },
{ { L"Armenian" }, { L"hy" } },
{ { L"Azerbaijani" }, { L"az" } },
{ { L"Basque" }, { L"eu" } },
{ { L"Belarusian" }, { L"be" } },
{ { L"Bengali" }, { L"bn" } },
{ { L"Bosnian" }, { L"bs" } },
{ { L"Bulgarian" }, { L"bg" } },
{ { L"Catalan" }, { L"ca" } },
{ { L"Cebuano" }, { L"ceb" } },
{ { L"Chichewa" }, { L"ny" } },
{ { L"Chinese (Simplified)" }, { L"zh-CN" } },
{ { L"Chinese (Traditional)" }, { L"zh-TW" } },
{ { L"Corsican" }, { L"co" } },
{ { L"Croatian" }, { L"hr" } },
{ { L"Czech" }, { L"cs" } },
{ { L"Danish" }, { L"da" } },
{ { L"Dutch" }, { L"nl" } },
{ { L"English" }, { L"en" } },
{ { L"Esperanto" }, { L"eo" } },
{ { L"Estonian" }, { L"et" } },
{ { L"Filipino" }, { L"tl" } },
{ { L"Finnish" }, { L"fi" } },
{ { L"French" }, { L"fr" } },
{ { L"Frisian" }, { L"fy" } },
{ { L"Galician" }, { L"gl" } },
{ { L"Georgian" }, { L"ka" } },
{ { L"German" }, { L"de" } },
{ { L"Greek" }, { L"el" } },
{ { L"Gujarati" }, { L"gu" } },
{ { L"Haitian Creole" }, { L"ht" } },
{ { L"Hausa" }, { L"ha" } },
{ { L"Hawaiian" }, { L"haw" } },
{ { L"Hebrew" }, { L"iw" } },
{ { L"Hindi" }, { L"hi" } },
{ { L"Hmong" }, { L"hmn" } },
{ { L"Hungarian" }, { L"hu" } },
{ { L"Icelandic" }, { L"is" } },
{ { L"Igbo" }, { L"ig" } },
{ { L"Indonesian" }, { L"id" } },
{ { L"Irish" }, { L"ga" } },
{ { L"Italian" }, { L"it" } },
{ { L"Japanese" }, { L"ja" } },
{ { L"Javanese" }, { L"jw" } },
{ { L"Kannada" }, { L"kn" } },
{ { L"Kazakh" }, { L"kk" } },
{ { L"Khmer" }, { L"km" } },
{ { L"Kinyarwanda" }, { L"rw" } },
{ { L"Korean" }, { L"ko" } },
{ { L"Kurdish (Kurmanji)" }, { L"ku" } },
{ { L"Kyrgyz" }, { L"ky" } },
{ { L"Lao" }, { L"lo" } },
{ { L"Latin" }, { L"la" } },
{ { L"Latvian" }, { L"lv" } },
{ { L"Lithuanian" }, { L"lt" } },
{ { L"Luxembourgish" }, { L"lb" } },
{ { L"Macedonian" }, { L"mk" } },
{ { L"Malagasy" }, { L"mg" } },
{ { L"Malay" }, { L"ms" } },
{ { L"Malayalam" }, { L"ml" } },
{ { L"Maltese" }, { L"mt" } },
{ { L"Maori" }, { L"mi" } },
{ { L"Marathi" }, { L"mr" } },
{ { L"Mongolian" }, { L"mn" } },
{ { L"Myanmar (Burmese)" }, { L"my" } },
{ { L"Nepali" }, { L"ne" } },
{ { L"Norwegian" }, { L"no" } },
{ { L"Odia (Oriya)" }, { L"or" } },
{ { L"Pashto" }, { L"ps" } },
{ { L"Persian" }, { L"fa" } },
{ { L"Polish" }, { L"pl" } },
{ { L"Portuguese" }, { L"pt" } },
{ { L"Punjabi" }, { L"pa" } },
{ { L"Romanian" }, { L"ro" } },
{ { L"Russian" }, { L"ru" } },
{ { L"Samoan" }, { L"sm" } },
{ { L"Scots Gaelic" }, { L"gd" } },
{ { L"Serbian" }, { L"sr" } },
{ { L"Sesotho" }, { L"st" } },
{ { L"Shona" }, { L"sn" } },
{ { L"Sindhi" }, { L"sd" } },
{ { L"Sinhala" }, { L"si" } },
{ { L"Slovak" }, { L"sk" } },
{ { L"Slovenian" }, { L"sl" } },
{ { L"Somali" }, { L"so" } },
{ { L"Spanish" }, { L"es" } },
{ { L"Sundanese" }, { L"su" } },
{ { L"Swahili" }, { L"sw" } },
{ { L"Swedish" }, { L"sv" } },
{ { L"Tajik" }, { L"tg" } },
{ { L"Tamil" }, { L"ta" } },
{ { L"Tatar" }, { L"tt" } },
{ { L"Telugu" }, { L"te" } },
{ { L"Thai" }, { L"th" } },
{ { L"Turkish" }, { L"tr" } },
{ { L"Turkmen" }, { L"tk" } },
{ { L"Ukrainian" }, { L"uk" } },
{ { L"Urdu" }, { L"ur" } },
{ { L"Uyghur" }, { L"ug" } },
{ { L"Uzbek" }, { L"uz" } },
{ { L"Vietnamese" }, { L"vi" } },
{ { L"Welsh" }, { L"cy" } },
{ { L"Xhosa" }, { L"xh" } },
{ { L"Yiddish" }, { L"yi" } },
{ { L"Yoruba" }, { L"yo" } },
{ { L"Zulu" }, { L"zu" } },
{ { L"?" }, { L"auto" } }
};
bool translateSelectedOnly = false, useRateLimiter = true, rateLimitSelected = false, useCache = true, useFilter = true;
int tokenCount = 30, rateLimitTimespan = 60000, maxSentenceSize = 1000;
std::pair<bool, std::wstring> Translate(const std::wstring& text, TranslationParam tlp)
{
if (!tlp.authKey.empty())
{
std::wstring translateFromComponent = tlp.translateFrom == L"?" ? L"" : L"&source=" + codes.at(tlp.translateFrom);
if (HttpRequest httpRequest{
L"Mozilla/5.0 Textractor",
L"translation.googleapis.com",
L"POST",
FormatString(L"/language/translate/v2?format=text&target=%s&key=%s%s", codes.at(tlp.translateTo), tlp.authKey, translateFromComponent).c_str(),
FormatString(R"({"q":["%s"]})", JSON::Escape(WideStringToString(text)))
})
if (auto translation = Copy(JSON::Parse(httpRequest.response)[L"data"][L"translations"][0][L"translatedText"].String())) return { true, translation.value() };
else return { false, FormatString(L"%s: %s", TRANSLATION_ERROR, httpRequest.response) };
else return { false, FormatString(L"%s (code=%u)", TRANSLATION_ERROR, httpRequest.errorCode) };
}
if (HttpRequest httpRequest{
L"Mozilla/5.0 Textractor",
L"translate.google.com",
L"GET",
FormatString(L"/m?sl=%s&tl=%s&q=%s", codes.at(tlp.translateFrom), codes.at(tlp.translateTo), Escape(text)).c_str()
})
{
auto start = httpRequest.response.find(L"result-container\">"), end = httpRequest.response.find(L'<', start);
if (end != std::string::npos) return { true, HTML::Unescape(httpRequest.response.substr(start + 18, end - start - 18)) };
return { false, FormatString(L"%s: %s", TRANSLATION_ERROR, httpRequest.response) };
}
else return { false, FormatString(L"%s (code=%u)", TRANSLATION_ERROR, httpRequest.errorCode) };
}

View File

@ -0,0 +1,65 @@
#include "network.h"
HttpRequest::HttpRequest(
const wchar_t* agentName,
const wchar_t* serverName,
const wchar_t* action,
const wchar_t* objectName,
std::string body,
const wchar_t* headers,
DWORD port,
const wchar_t* referrer,
DWORD requestFlags,
const wchar_t* httpVersion,
const wchar_t** acceptTypes
)
{
static std::atomic<HINTERNET> internet = NULL;
if (!internet) internet = WinHttpOpen(agentName, WINHTTP_ACCESS_TYPE_DEFAULT_PROXY, NULL, NULL, 0);
if (internet)
if (InternetHandle connection = WinHttpConnect(internet, serverName, port, 0))
if (InternetHandle request = WinHttpOpenRequest(connection, action, objectName, httpVersion, referrer, acceptTypes, requestFlags))
if (WinHttpSendRequest(request, headers, -1UL, body.empty() ? NULL : body.data(), body.size(), body.size(), NULL))
{
WinHttpReceiveResponse(request, NULL);
//DWORD size = 0;
//WinHttpQueryHeaders(request, WINHTTP_QUERY_RAW_HEADERS_CRLF, WINHTTP_HEADER_NAME_BY_INDEX, NULL, &size, WINHTTP_NO_HEADER_INDEX);
//this->headers.resize(size);
//WinHttpQueryHeaders(request, WINHTTP_QUERY_RAW_HEADERS_CRLF, WINHTTP_HEADER_NAME_BY_INDEX, this->headers.data(), &size, WINHTTP_NO_HEADER_INDEX);
std::string data;
DWORD availableSize, downloadedSize;
do
{
availableSize = 0;
WinHttpQueryDataAvailable(request, &availableSize);
if (!availableSize) break;
std::vector<char> buffer(availableSize);
WinHttpReadData(request, buffer.data(), availableSize, &downloadedSize);
data.append(buffer.data(), downloadedSize);
} while (availableSize > 0);
response = StringToWideString(data);
this->connection = std::move(connection);
this->request = std::move(request);
}
else errorCode = GetLastError();
else errorCode = GetLastError();
else errorCode = GetLastError();
else errorCode = GetLastError();
}
std::wstring Escape(const std::wstring& text)
{
std::wstring escaped;
for (unsigned char ch : WideStringToString(text)) escaped += FormatString(L"%%%02X", (int)ch);
return escaped;
}
std::string Escape(const std::string& text)
{
std::string escaped;
for (unsigned char ch : text) escaped += FormatString("%%%02X", (int)ch);
return escaped;
}
TEST(assert(JSON::Parse<wchar_t>(LR"([{"string":"hello world","boolean":false,"number":1.67e+4,"null":null,"array":[]},"hello world"])")));

View File

@ -0,0 +1,233 @@
#pragma once
#include <winhttp.h>
#include <variant>
using InternetHandle = AutoHandle<Functor<WinHttpCloseHandle>>;
struct HttpRequest
{
HttpRequest(
const wchar_t* agentName,
const wchar_t* serverName,
const wchar_t* action,
const wchar_t* objectName,
std::string body = "",
const wchar_t* headers = NULL,
DWORD port = INTERNET_DEFAULT_PORT,
const wchar_t* referrer = NULL,
DWORD requestFlags = WINHTTP_FLAG_SECURE | WINHTTP_FLAG_ESCAPE_DISABLE,
const wchar_t* httpVersion = NULL,
const wchar_t** acceptTypes = NULL
);
operator bool() { return errorCode == ERROR_SUCCESS; }
std::wstring response;
std::wstring headers;
InternetHandle connection = NULL;
InternetHandle request = NULL;
DWORD errorCode = ERROR_SUCCESS;
};
std::wstring Escape(const std::wstring& text);
std::string Escape(const std::string& text);
namespace HTML
{
template <typename C>
std::basic_string<C> Unescape(std::basic_string<C> text)
{
constexpr C
lt[] = { '&', 'l', 't', ';' },
gt[] = { '&', 'g', 't', ';' },
apos1[] = { '&', 'a', 'p', 'o', 's', ';' },
apos2[] = { '&', '#', '3', '9', ';' },
apos3[] = { '&', '#', 'x', '2', '7', ';' },
apos4[] = { '&', '#', 'X', '2', '7', ';' },
quot[] = { '&', 'q', 'u', 'o', 't', ';' },
amp[] = { '&', 'a', 'm', 'p', ';' };
for (int i = 0; i < text.size(); ++i)
if (text[i] == '&')
for (auto [original, length, replacement] : Array<const C*, size_t, C>{
{ lt, std::size(lt), '<' },
{ gt, std::size(gt), '>' },
{ apos1, std::size(apos1), '\'' },
{ apos2, std::size(apos2), '\'' },
{ apos3, std::size(apos3), '\'' },
{ apos4, std::size(apos4), '\'' },
{ quot, std::size(quot), '"' },
{ amp, std::size(amp), '&' }
}) if (std::char_traits<C>::compare(text.data() + i, original, length) == 0) text.replace(i, length, 1, replacement);
return text;
}
}
namespace JSON
{
template <typename C>
std::basic_string<C> Escape(std::basic_string<C> text)
{
int oldSize = text.size();
text.resize(text.size() + std::count_if(text.begin(), text.end(), [](C ch) { return ch == '\n' || ch == '\r' || ch == '\t' || ch == '\\' || ch == '"'; }));
auto out = text.rbegin();
for (int i = oldSize - 1; i >= 0; --i)
{
if (text[i] == '\n') *out++ = 'n';
else if (text[i] == '\t') *out++ = 't';
else if (text[i] == '\r') *out++ = 'r';
else if (text[i] == '\\' || text[i] == '"') *out++ = text[i];
else
{
*out++ = text[i];
continue;
}
*out++ = '\\';
}
text.erase(std::remove_if(text.begin(), text.end(), [](uint64_t ch) { return ch < 0x20 || ch == 0x7f; }), text.end());
return text;
}
template <typename C> struct UTF {};
template <> struct UTF<wchar_t>
{
inline static std::wstring FromCodepoint(unsigned codepoint) { return { (wchar_t)codepoint }; } // TODO: surrogate pairs
};
template <typename C>
struct Value : private std::variant<std::monostate, std::nullptr_t, bool, double, std::basic_string<C>, std::vector<Value<C>>, std::unordered_map<std::basic_string<C>, Value<C>>>
{
using std::variant<std::monostate, std::nullptr_t, bool, double, std::basic_string<C>, std::vector<Value<C>>, std::unordered_map<std::basic_string<C>, Value<C>>>::variant;
explicit operator bool() const { return index(); }
bool IsNull() const { return index() == 1; }
auto Boolean() const { return std::get_if<bool>(this); }
auto Number() const { return std::get_if<double>(this); }
auto String() const { return std::get_if<std::basic_string<C>>(this); }
auto Array() const { return std::get_if<std::vector<Value<C>>>(this); }
auto Object() const { return std::get_if<std::unordered_map<std::basic_string<C>, Value<C>>>(this); }
const Value<C>& operator[](std::basic_string<C> key) const
{
if (auto object = Object()) if (auto it = object->find(key); it != object->end()) return it->second;
return failure;
}
const Value<C>& operator[](int i) const
{
if (auto array = Array()) if (i < array->size()) return array->at(i);
return failure;
}
static const Value<C> failure;
};
template <typename C> const Value<C> Value<C>::failure;
template <typename C, int maxDepth = 25>
Value<C> Parse(const std::basic_string<C>& text, int64_t& i, int depth)
{
if (depth > maxDepth) return {};
C ch;
auto SkipWhitespace = [&]
{
while (i < text.size() && (text[i] == ' ' || text[i] == '\n' || text[i] == '\r' || text[i] == '\t')) ++i;
if (i >= text.size()) return true;
ch = text[i];
return false;
};
auto ExtractString = [&]
{
std::basic_string<C> unescaped;
i += 1;
for (; i < text.size(); ++i)
{
auto ch = text[i];
if (ch == '"') return i += 1, unescaped;
if (ch == '\\')
{
ch = text[i + 1];
if (ch == 'u' && isxdigit(text[i + 2]) && isxdigit(text[i + 3]) && isxdigit(text[i + 4]) && isxdigit(text[i + 5]))
{
char charCode[] = { (char)text[i + 2], (char)text[i + 3], (char)text[i + 4], (char)text[i + 5], 0 };
unescaped += UTF<C>::FromCodepoint(strtoul(charCode, nullptr, 16));
i += 5;
continue;
}
for (auto [original, value] : Array<char, char>{ { 'b', '\b' }, {'f', '\f'}, {'n', '\n'}, {'r', '\r'}, {'t', '\t'} }) if (ch == original)
{
unescaped.push_back(value);
goto replaced;
}
unescaped.push_back(ch);
replaced: i += 1;
}
else unescaped.push_back(ch);
}
return unescaped;
};
if (SkipWhitespace()) return {};
constexpr C nullStr[] = { 'n', 'u', 'l', 'l' }, trueStr[] = { 't', 'r', 'u', 'e' }, falseStr[] = { 'f', 'a', 'l', 's', 'e' };
if (ch == nullStr[0])
if (std::char_traits<C>::compare(text.data() + i, nullStr, std::size(nullStr)) == 0) return i += std::size(nullStr), nullptr;
else return {};
if (ch == trueStr[0])
if (std::char_traits<C>::compare(text.data() + i, trueStr, std::size(trueStr)) == 0) return i += std::size(trueStr), true;
else return {};
if (ch == falseStr[0])
if (std::char_traits<C>::compare(text.data() + i, falseStr, std::size(falseStr)) == 0) return i += std::size(falseStr), false;
else return {};
if (ch == '-' || (ch >= '0' && ch <= '9'))
{
std::string number;
for (; i < text.size() && ((text[i] >= '0' && text[i] <= '9') || text[i] == '-' || text[i] == '+' || text[i] == 'e' || text[i] == 'E' || text[i] == '.'); ++i)
number.push_back(text[i]);
return strtod(number.c_str(), NULL);
}
if (ch == '"') return ExtractString();
if (ch == '[')
{
std::vector<Value<C>> array;
while (true)
{
i += 1;
if (SkipWhitespace()) return {};
if (ch == ']') return i += 1, Value<C>(array);
if (!array.emplace_back(Parse<C, maxDepth>(text, i, depth + 1))) return {};
if (SkipWhitespace()) return {};
if (ch == ']') return i += 1, Value<C>(array);
if (ch != ',') return {};
}
}
if (ch == '{')
{
std::unordered_map<std::basic_string<C>, Value<C>> object;
while (true)
{
i += 1;
if (SkipWhitespace()) return {};
if (ch == '}') return i += 1, Value<C>(object);
if (ch != '"') return {};
auto key = ExtractString();
if (SkipWhitespace() || ch != ':') return {};
i += 1;
if (!(object[std::move(key)] = Parse<C, maxDepth>(text, i, depth + 1))) return {};
if (SkipWhitespace()) return {};
if (ch == '}') return i += 1, Value<C>(object);
if (ch != ',') return {};
}
}
return {};
}
template <typename C>
Value<C> Parse(const std::basic_string<C>& text)
{
int64_t start = 0;
return Parse(text, start, 0);
}
}

View File

@ -0,0 +1,38 @@
#pragma once
#include <QString>
#include <QVector>
#include <QHash>
#include <QFile>
#include <QFileInfo>
#include <QDir>
#include <QSettings>
#include <QMainWindow>
#include <QDialog>
#include <QApplication>
#include <QLayout>
#include <QFormLayout>
#include <QLabel>
#include <QPushButton>
#include <QCheckBox>
#include <QSpinBox>
#include <QListWidget>
#include <QMessageBox>
#include <QInputDialog>
static thread_local bool ok;
constexpr auto CONFIG_FILE = u8"Textractor.ini";
constexpr auto WINDOW = u8"Window";
struct Settings : QSettings { Settings(QObject* parent = nullptr) : QSettings(CONFIG_FILE, QSettings::IniFormat, parent) {} };
struct QTextFile : QFile { QTextFile(QString name, QIODevice::OpenMode mode) : QFile(name) { open(mode | QIODevice::Text); } };
struct Localizer { Localizer() { Localize(); } };
inline std::wstring S(const QString& s) { return { s.toStdWString() }; }
inline QString S(const std::string& s) { return QString::fromStdString(s); }
inline QString S(const std::wstring& s) { return QString::fromStdWString(s); }
// TODO: allow paired surrogates
inline void sanitize(QString& s) { s.chop(std::distance(std::remove_if(s.begin(), s.end(), [](QChar ch) { return ch.isSurrogate(); }), s.end())); }
inline QString sanitize(QString&& s) { sanitize(s); return std::move(s); }

View File

@ -0,0 +1,71 @@
#include "qtcommon.h"
#include "extension.h"
#include "ui_regexfilter.h"
#include "blockmarkup.h"
#include <fstream>
extern const char* REGEX_FILTER;
extern const char* INVALID_REGEX;
extern const char* CURRENT_FILTER;
const char* REGEX_SAVE_FILE = "SavedRegexFilters.txt";
std::optional<std::wregex> regex;
std::wstring replace = L"$1";
concurrency::reader_writer_lock m;
DWORD (*GetSelectedProcessId)() = [] { return 0UL; };
class Window : public QDialog, Localizer
{
public:
Window() : QDialog(nullptr, Qt::WindowMinMaxButtonsHint)
{
ui.setupUi(this);
connect(ui.regexEdit, &QLineEdit::textEdited, this, &Window::SetRegex);
connect(ui.saveButton, &QPushButton::clicked, this, &Window::Save);
setWindowTitle(REGEX_FILTER);
QMetaObject::invokeMethod(this, &QWidget::show, Qt::QueuedConnection);
}
void SetRegex(QString regex)
{
ui.regexEdit->setText(regex);
std::scoped_lock lock(m);
if (!regex.isEmpty()) try { ::regex = S(regex); }
catch (std::regex_error) { return ui.output->setText(INVALID_REGEX); }
else ::regex = std::nullopt;
ui.output->setText(QString(CURRENT_FILTER).arg(regex));
}
private:
void Save()
{
auto formatted = FormatString(
L"\xfeff|PROCESS|%s|FILTER|%s|END|\r\n",
GetModuleFilename(GetSelectedProcessId()).value_or(FormatString(L"Error getting name of process 0x%X", GetSelectedProcessId())),
S(ui.regexEdit->text())
);
std::ofstream(REGEX_SAVE_FILE, std::ios::binary | std::ios::app).write((const char*)formatted.c_str(), formatted.size() * sizeof(wchar_t));
}
Ui::FilterWindow ui;
} window;
bool ProcessSentence(std::wstring& sentence, SentenceInfo sentenceInfo)
{
static auto _ = GetSelectedProcessId = (DWORD(*)())sentenceInfo["get selected process id"];
if (sentenceInfo["text number"] == 0) return false;
if (/*sentenceInfo["current select"] && */!regex) if (auto processName = GetModuleFilename(sentenceInfo["process id"]))
{
std::ifstream stream(REGEX_SAVE_FILE, std::ios::binary);
BlockMarkupIterator savedFilters(stream, Array<std::wstring_view>{ L"|PROCESS|", L"|FILTER|" });
std::vector<std::wstring> regexes;
while (auto read = savedFilters.Next()) if (read->at(0) == processName) regexes.push_back(std::move(read->at(1)));
if (!regexes.empty()) QMetaObject::invokeMethod(&window, std::bind(&Window::SetRegex, &window, S(regexes.back())), Qt::BlockingQueuedConnection);
}
concurrency::reader_writer_lock::scoped_lock_read readLock(m);
if (regex) sentence = std::regex_replace(sentence, regex.value(), replace);
return true;
}

View File

@ -0,0 +1,61 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>FilterWindow</class>
<widget class="QDialog">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>500</width>
<height>80</height>
</rect>
</property>
<layout class="QVBoxLayout">
<item>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QLineEdit" name="regexEdit"/>
</item>
<item>
<widget class="QPushButton" name="saveButton">
<property name="text">
<string>Save</string>
</property>
</widget>
</item>
</layout>
</item>
<item>
<widget class="QLabel" name="output">
<property name="text">
<string/>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
</widget>
</item>
<item>
<widget class="QLabel">
<property name="text">
<string>&lt;a href=&quot;https://regexr.com&quot;&gt;regexr.com&lt;/a&gt;</string>
</property>
<property name="textFormat">
<enum>Qt::RichText</enum>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
<property name="openExternalLinks">
<bool>true</bool>
</property>
<property name="textInteractionFlags">
<set>Qt::TextBrowserInteraction</set>
</property>
</widget>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>

View File

@ -0,0 +1,71 @@
#include "extension.h"
#include "blockmarkup.h"
#include <fstream>
#include <process.h>
extern const wchar_t* REGEX_REPLACER_INSTRUCTIONS;
const char* REPLACE_SAVE_FILE = "SavedRegexReplacements.txt";
std::atomic<std::filesystem::file_time_type> replaceFileLastWrite = {};
concurrency::reader_writer_lock m;
std::vector<std::tuple<std::wregex, std::wstring, std::regex_constants::match_flag_type>> replacements;
void UpdateReplacements()
{
try
{
if (replaceFileLastWrite.exchange(std::filesystem::last_write_time(REPLACE_SAVE_FILE)) == std::filesystem::last_write_time(REPLACE_SAVE_FILE)) return;
std::scoped_lock lock(m);
replacements.clear();
std::ifstream stream(REPLACE_SAVE_FILE, std::ios::binary);
BlockMarkupIterator savedFilters(stream, Array<std::wstring_view>{ L"|REGEX|", L"|BECOMES|", L"|MODIFIER|" });
while (auto read = savedFilters.Next())
{
const auto& [regex, replacement, modifier] = read.value();
try
{
replacements.emplace_back(
std::wregex(regex, modifier.find(L'i') == std::string::npos ? std::regex::ECMAScript : std::regex::icase),
replacement,
modifier.find(L'g') == std::string::npos ? std::regex_constants::format_first_only : std::regex_constants::format_default
);
}
catch (std::regex_error) {}
}
}
catch (std::filesystem::filesystem_error) { replaceFileLastWrite.store({}); }
}
BOOL WINAPI DllMain(HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved)
{
switch (ul_reason_for_call)
{
case DLL_PROCESS_ATTACH:
{
UpdateReplacements();
if (replacements.empty())
{
auto file = std::ofstream(REPLACE_SAVE_FILE, std::ios::binary) << "\xff\xfe";
for (auto ch : std::wstring_view(REGEX_REPLACER_INSTRUCTIONS))
file << (ch == L'\n' ? std::string_view("\r\0\n", 4) : std::string_view((char*)&ch, 2));
SpawnThread([] { _spawnlp(_P_DETACH, "notepad", "notepad", REPLACE_SAVE_FILE, NULL); }); // show file to user
}
}
break;
case DLL_PROCESS_DETACH:
{
}
break;
}
return TRUE;
}
bool ProcessSentence(std::wstring& sentence, SentenceInfo sentenceInfo)
{
UpdateReplacements();
concurrency::reader_writer_lock::scoped_lock_read readLock(m);
for (const auto& [regex, replacement, flags] : replacements) sentence = std::regex_replace(sentence, regex, replacement, flags);
return true;
}

View File

@ -0,0 +1,54 @@
#include "extension.h"
bool ProcessSentence(std::wstring& sentence, SentenceInfo sentenceInfo)
{
if (sentenceInfo["text number"] == 0) return false;
std::vector<int> repeatNumbers(sentence.size() + 1, 0);
for (int i = 0; i < sentence.size(); ++i)
{
if (sentence[i] != sentence[i + 1])
{
int j = i;
while (sentence[j] == sentence[i] && --j >= 0);
repeatNumbers[i - j] += 1;
}
}
int repeatNumber = std::distance(repeatNumbers.begin(), std::max_element(repeatNumbers.rbegin(), repeatNumbers.rend()).base() - 1);
if (repeatNumber < 2) return false;
std::wstring newSentence;
for (int i = 0; i < sentence.size();)
{
newSentence.push_back(sentence[i]);
for (int j = i; j <= sentence.size(); ++j)
{
if (j == sentence.size() || sentence[i] != sentence[j])
{
i += (j - i) % repeatNumber == 0 ? repeatNumber : 1;
break;
}
}
}
sentence = newSentence;
return true;
}
TEST(
{
InfoForExtension nonConsole[] = { { "text number", 1 }, {} };
std::wstring repeatedChars = L"aaaaaaaaaaaabbbbbbcccdddaabbbcccddd";
std::wstring someRepeatedChars = L"abcdefaabbccddeeff";
ProcessSentence(repeatedChars, { nonConsole });
ProcessSentence(someRepeatedChars, { nonConsole });
assert(repeatedChars.find(L"aaaabbcd") == 0);
assert(someRepeatedChars == L"abcdefabcdef");
std::wstring empty = L"", one = L" ", normal = L"This is a normal sentence. はい";
ProcessSentence(empty, { nonConsole });
ProcessSentence(one, { nonConsole });
ProcessSentence(normal, { nonConsole });
assert(empty == L"" && one == L" " && normal == L"This is a normal sentence. はい");
}
);

View File

@ -0,0 +1,97 @@
#include "extension.h"
std::vector<int> GenerateSuffixArray(const std::wstring& text)
{
std::vector<int> suffixArray(text.size());
for (int i = 0; i < text.size(); ++i) suffixArray[i] = i;
// The below code is a more efficient way of doing this:
// std::sort(suffixArray.begin(), suffixArray.end(), [&](int a, int b) { return wcscmp(text.c_str() + a, text.c_str() + b) > 0; });
std::stable_sort(suffixArray.begin(), suffixArray.end(), [&](int a, int b) { return text[a] > text[b]; });
std::vector<int> eqClasses(text.begin(), text.end());
std::vector<int> count(text.size());
for (int length = 1; length < text.size(); length *= 2)
{
// Determine equivalence class up to length, by checking length / 2 equivalence of suffixes and their following length / 2 suffixes
std::vector<int> prevEqClasses = eqClasses;
eqClasses[suffixArray[0]] = 0;
for (int i = 1; i < text.size(); ++i)
{
int currentSuffix = suffixArray[i], lastSuffix = suffixArray[i - 1];
if (currentSuffix + length < text.size() && prevEqClasses[currentSuffix] == prevEqClasses[lastSuffix] &&
prevEqClasses[currentSuffix + length / 2] == prevEqClasses[lastSuffix + length / 2]
)
eqClasses[currentSuffix] = eqClasses[lastSuffix];
else eqClasses[currentSuffix] = i;
}
// Sort within equivalence class based on order of following suffix after length (orders up to length * 2)
for (int i = 0; i < text.size(); ++i) count[i] = i;
for (auto suffix : std::vector(suffixArray))
{
int precedingSuffix = suffix - length;
if (precedingSuffix >= 0) suffixArray[count[eqClasses[precedingSuffix]]++] = precedingSuffix;
}
}
for (int i = 0; i + 1 < text.size(); ++i)
assert(wcscmp(text.c_str() + suffixArray[i], text.c_str() + suffixArray[i + 1]) > 0);
return suffixArray;
}
constexpr wchar_t ERASED = 0xf246; // inside Unicode private use area
bool ProcessSentence(std::wstring& sentence, SentenceInfo sentenceInfo)
{
if (sentenceInfo["text number"] == 0) return false;
// This algorithm looks for repeating substrings (in other words, common prefixes among the set of suffixes) of the sentence with length > 6
// It then looks for any regions of characters at least twice as long as the substring made up only of characters in the substring, and erases them
// If this results in the substring being completely erased from the string, the substring is copied to the last location where it was located in the original string
auto timeout = GetTickCount64() + 30'000; // give up if taking over 30 seconds
std::vector<int> suffixArray = GenerateSuffixArray(sentence);
for (int i = 0; i + 1 < sentence.size() && GetTickCount64() < timeout; ++i)
{
int commonPrefixLength = 0;
for (int j = suffixArray[i], k = suffixArray[i + 1]; j < sentence.size() && k < sentence.size(); ++j, ++k)
if (sentence[j] != ERASED && sentence[j] == sentence[k]) commonPrefixLength += 1;
else break;
if (commonPrefixLength > 6)
{
std::wstring substring(sentence, suffixArray[i], commonPrefixLength);
bool substringCharMap[0x10000] = {};
for (auto ch : substring) substringCharMap[ch] = true;
for (int regionSize = 0, j = 0; j <= sentence.size(); ++j)
if (substringCharMap[sentence[j]]) regionSize += 1;
else if (regionSize >= commonPrefixLength * 2)
while (regionSize > 0) sentence[j - regionSize--] = ERASED;
else regionSize = 0;
if (!wcsstr(sentence.c_str(), substring.c_str())) std::copy(substring.begin(), substring.end(), sentence.begin() + max(suffixArray[i], suffixArray[i + 1]));
}
}
sentence.erase(std::remove(sentence.begin(), sentence.end(), ERASED), sentence.end());
return true;
}
TEST(
{
InfoForExtension nonConsole[] = { { "text number", 1 }, {} };
std::wstring cyclicRepeats = L"Name: '_abcdefg_abcdefg_abcdefg_abcdefg_abcdefg'";
std::wstring buildupRepeats = L"Name: '__a_ab_abc_abcd_abcde_abcdef_abcdefg'";
std::wstring breakdownRepeats = L"Name: '_abcdefg_abcdef_abcde_abcd_abc_ab_a_'";
ProcessSentence(cyclicRepeats, { nonConsole });
ProcessSentence(buildupRepeats, { nonConsole });
ProcessSentence(breakdownRepeats, { nonConsole });
assert(cyclicRepeats == L"Name: '_abcdefg'");
assert(buildupRepeats == L"Name: '_abcdefg'");
assert(breakdownRepeats == L"Name: '_abcdefg'");
std::wstring empty = L"", one = L" ", normal = L"This is a normal sentence. はい";
ProcessSentence(empty, { nonConsole });
ProcessSentence(one, { nonConsole });
ProcessSentence(normal, { nonConsole });
assert(empty == L"" && one == L" " && normal == L"This is a normal sentence. はい");
}
);

View File

@ -0,0 +1,52 @@
#include "extension.h"
bool ProcessSentence(std::wstring& sentence, SentenceInfo sentenceInfo)
{
if (sentenceInfo["text number"] == 0) return false;
// This algorithm looks at all the prefixes of the sentence: if a prefix is found later in the sentence, it is removed from the beginning and the process is repeated
auto timeout = GetTickCount64() + 30'000; // give up if taking over 30 seconds
auto data = std::make_unique<wchar_t[]>(sentence.size() + 1);
wcscpy_s(data.get(), sentence.size() + 1, sentence.c_str());
wchar_t* dataEnd = data.get() + sentence.size();
int skip = 0, count = 0;
for (wchar_t* end = dataEnd; end - data.get() > skip && GetTickCount64() < timeout; --end)
{
std::swap(*end, *dataEnd);
int junkLength = end - data.get() - skip;
auto junkFound = wcsstr(sentence.c_str() + skip + junkLength, data.get() + skip);
std::swap(*end, *dataEnd);
if (junkFound)
{
if (count && junkLength < min(skip / count, 4)) break;
skip += junkLength;
count += 1;
end = dataEnd;
}
}
if (count && skip / count >= 3)
{
sentence = data.get() + skip;
return true;
}
return false;
}
TEST(
{
InfoForExtension nonConsole[] = { { "text number", 1 }, {} };
std::wstring cyclicRepeats = L"_abcde_abcdef_abcdefg_abcdefg_abcdefg_abcdefg_abcdefg";
std::wstring buildupRepeats = L"__a_ab_abc_abcd_abcde_abcdef_abcdefg";
ProcessSentence(cyclicRepeats, { nonConsole });
ProcessSentence(buildupRepeats, { nonConsole });
assert(cyclicRepeats == L"_abcdefg");
assert(buildupRepeats == L"_abcdefg");
std::wstring empty = L"", one = L" ", normal = L"This is a normal sentence. はい";
ProcessSentence(empty, { nonConsole });
ProcessSentence(one, { nonConsole });
ProcessSentence(normal, { nonConsole });
assert(empty == L"" && one == L" " && normal == L"This is a normal sentence. はい");
}
);

View File

@ -0,0 +1,44 @@
#include "extension.h"
int sentenceCacheSize = 30;
BOOL WINAPI DllMain(HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved)
{
switch (ul_reason_for_call)
{
case DLL_PROCESS_ATTACH:
{
wchar_t filePath[MAX_PATH];
GetModuleFileNameW(hModule, filePath, MAX_PATH);
if (wchar_t* fileName = wcsrchr(filePath, L'\\')) swscanf_s(fileName, L"\\Remove %d Repeated Sentences.xdll", &sentenceCacheSize);
}
break;
case DLL_PROCESS_DETACH:
{
}
break;
}
return TRUE;
}
bool ProcessSentence(std::wstring& sentence, SentenceInfo sentenceInfo)
{
uint64_t textNumber = sentenceInfo["text number"];
if (textNumber == 0) return false;
static std::deque<Synchronized<std::vector<std::wstring>>> cache;
static std::mutex m;
m.lock();
if (textNumber + 1 > cache.size()) cache.resize(textNumber + 1);
auto prevSentences = cache[textNumber].Acquire();
m.unlock();
auto& inserted = prevSentences->emplace_back(sentence);
auto firstLocation = std::find(prevSentences->begin(), prevSentences->end(), sentence);
if (&*firstLocation != &inserted)
{
prevSentences->erase(firstLocation);
sentence.clear();
}
if (prevSentences->size() > sentenceCacheSize) prevSentences->erase(prevSentences->begin());
return sentence.empty();
}

View File

@ -0,0 +1,142 @@
#include "extension.h"
#include "blockmarkup.h"
#include <cwctype>
#include <fstream>
#include <sstream>
#include <process.h>
extern const wchar_t* REPLACER_INSTRUCTIONS;
constexpr auto REPLACE_SAVE_FILE = u8"SavedReplacements.txt";
std::atomic<std::filesystem::file_time_type> replaceFileLastWrite = {};
concurrency::reader_writer_lock m;
class Trie
{
public:
Trie(const std::istream& replacementScript)
{
BlockMarkupIterator replacementScriptParser(replacementScript, Array<std::wstring_view>{ L"|ORIG|", L"|BECOMES|" });
while (auto read = replacementScriptParser.Next())
{
const auto& [original, replacement] = read.value();
Node* current = &root;
for (auto ch : original) if (!Ignore(ch)) current = Next(current, ch);
if (current != &root)
current->value = charStorage.insert(charStorage.end(), replacement.c_str(), replacement.c_str() + replacement.size() + 1) - charStorage.begin();
}
}
std::wstring Replace(const std::wstring& sentence) const
{
std::wstring result;
for (int i = 0; i < sentence.size();)
{
std::wstring_view replacement(sentence.c_str() + i, 1);
int originalLength = 1;
const Node* current = &root;
for (int j = i; current && j <= sentence.size(); ++j)
{
if (current->value >= 0)
{
replacement = charStorage.data() + current->value;
originalLength = j - i;
}
if (!Ignore(sentence[j])) current = Next(current, sentence[j]) ? Next(current, sentence[j]) : Next(current, L'^');
}
result += replacement;
i += originalLength;
}
return result;
}
bool Empty()
{
return root.charMap.empty();
}
private:
static bool Ignore(wchar_t ch)
{
return ch <= 0x20 || iswspace(ch);
}
template <typename Node>
static Node* Next(Node* node, wchar_t ch)
{
auto it = std::lower_bound(node->charMap.begin(), node->charMap.end(), ch, [](const auto& one, auto two) { return one.first < two; });
if (it != node->charMap.end() && it->first == ch) return it->second.get();
if constexpr (!std::is_const_v<Node>) return node->charMap.insert(it, { ch, std::make_unique<Node>() })->second.get();
return nullptr;
}
struct Node
{
std::vector<std::pair<wchar_t, std::unique_ptr<Node>>> charMap;
ptrdiff_t value = -1;
} root;
std::vector<wchar_t> charStorage;
} trie = { std::istringstream("") };
void UpdateReplacements()
{
try
{
if (replaceFileLastWrite.exchange(std::filesystem::last_write_time(REPLACE_SAVE_FILE)) == std::filesystem::last_write_time(REPLACE_SAVE_FILE)) return;
std::scoped_lock lock(m);
trie = Trie(std::ifstream(REPLACE_SAVE_FILE, std::ios::binary));
}
catch (std::filesystem::filesystem_error) { replaceFileLastWrite.store({}); }
}
BOOL WINAPI DllMain(HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved)
{
switch (ul_reason_for_call)
{
case DLL_PROCESS_ATTACH:
{
UpdateReplacements();
if (trie.Empty())
{
auto file = std::ofstream(REPLACE_SAVE_FILE, std::ios::binary) << "\xff\xfe";
for (auto ch : std::wstring_view(REPLACER_INSTRUCTIONS))
file << (ch == L'\n' ? std::string_view("\r\0\n", 4) : std::string_view((char*)&ch, 2));
SpawnThread([] { _spawnlp(_P_DETACH, "notepad", "notepad", REPLACE_SAVE_FILE, NULL); }); // show file to user
}
}
break;
case DLL_PROCESS_DETACH:
{
}
break;
}
return TRUE;
}
bool ProcessSentence(std::wstring& sentence, SentenceInfo)
{
UpdateReplacements();
concurrency::reader_writer_lock::scoped_lock_read readLock(m);
sentence = trie.Replace(sentence);
return true;
}
TEST(
{
std::wstring replacementScript = LR"(
|ORIG||BECOMES|goodbye |END|Ignore this text
And this text   
|ORIG||BECOMES|idiot|END|
|ORIG| |BECOMES| hello|END||ORIG|delet^this|BECOMES||END|)";
Trie replacements(std::istringstream(std::string{ (const char*)replacementScript.c_str(), replacementScript.size() * sizeof(wchar_t) }));
std::wstring original = LR"(Don't replace this 
delete this)";
std::wstring replaced = Trie(std::move(replacements)).Replace(original);
assert(replaced == L"Don't replace thisgoodbye idiot hello");
}
);

View File

@ -0,0 +1,54 @@
#include "qtcommon.h"
#include "extension.h"
#include <QPlainTextEdit>
extern const char* LOAD_SCRIPT;
constexpr auto STYLE_SAVE_FILE = u8"Textractor.qss";
class Window : public QDialog, Localizer
{
public:
Window() : QDialog(nullptr, Qt::WindowMinMaxButtonsHint)
{
connect(&loadButton, &QPushButton::clicked, this, &Window::LoadScript);
if (scriptEditor.toPlainText().isEmpty())
scriptEditor.setPlainText("/*\nhttps://www.google.com/search?q=Qt+stylesheet+gallery\nhttps://doc.qt.io/qt-5/stylesheet-syntax.html\n*/");
layout.addWidget(&scriptEditor);
layout.addWidget(&loadButton);
resize(800, 600);
setWindowTitle("Styler");
QMetaObject::invokeMethod(this, &QWidget::show, Qt::QueuedConnection);
LoadScript();
}
~Window()
{
qApp->setStyleSheet("");
Save();
}
private:
void LoadScript()
{
qApp->setStyleSheet(scriptEditor.toPlainText());
Save();
}
void Save()
{
QTextFile(STYLE_SAVE_FILE, QIODevice::WriteOnly | QIODevice::Truncate).write(scriptEditor.toPlainText().toUtf8());
}
QHBoxLayout layout{ this };
QPlainTextEdit scriptEditor{ QTextFile(STYLE_SAVE_FILE, QIODevice::ReadOnly).readAll(), this };
QPushButton loadButton{ LOAD_SCRIPT, this };
} window;
bool ProcessSentence(std::wstring& sentence, SentenceInfo sentenceInfo)
{
return false;
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,79 @@
#include "qtcommon.h"
#include "extension.h"
#include "ui_threadlinker.h"
#include <QKeyEvent>
extern const char* THREAD_LINKER;
extern const char* LINK;
extern const char* UNLINK;
extern const char* THREAD_LINK_FROM;
extern const char* THREAD_LINK_TO;
extern const char* HEXADECIMAL;
std::unordered_map<int64_t, std::unordered_set<int64_t>> links;
std::unordered_set<int64_t> universalLinks, empty;
bool separateSentences = false; // allow user to change?
concurrency::reader_writer_lock m;
class Window : public QDialog, Localizer
{
public:
Window() : QDialog(nullptr, Qt::WindowMinMaxButtonsHint)
{
ui.setupUi(this);
ui.linkButton->setText(LINK);
ui.unlinkButton->setText(UNLINK);
connect(ui.linkButton, &QPushButton::clicked, this, &Window::Link);
connect(ui.unlinkButton, &QPushButton::clicked, this, &Window::Unlink);
setWindowTitle(THREAD_LINKER);
QMetaObject::invokeMethod(this, &QWidget::show, Qt::QueuedConnection);
}
private:
void Link()
{
bool ok1, ok2, ok3, ok4;
QString fromInput = QInputDialog::getText(this, THREAD_LINK_FROM, HEXADECIMAL, QLineEdit::Normal, "All", &ok1, Qt::WindowCloseButtonHint);
int from = fromInput.toInt(&ok2, 16);
if (ok1 && (fromInput == "All" || ok2))
{
int to = QInputDialog::getText(this, THREAD_LINK_TO, HEXADECIMAL, QLineEdit::Normal, "", &ok3, Qt::WindowCloseButtonHint).toInt(&ok4, 16);
if (ok3 && ok4)
{
std::scoped_lock lock(m);
if ((ok2 ? links[from] : universalLinks).insert(to).second)
ui.linkList->addItem((ok2 ? QString::number(from, 16) : "All") + "->" + QString::number(to, 16));
}
}
}
void Unlink()
{
if (ui.linkList->currentItem())
{
QStringList link = ui.linkList->currentItem()->text().split("->");
ui.linkList->takeItem(ui.linkList->currentRow());
std::scoped_lock lock(m);
(link[0] == "All" ? universalLinks : links[link[0].toInt(nullptr, 16)]).erase(link[1].toInt(nullptr, 16));
}
}
void keyPressEvent(QKeyEvent* event) override
{
if (event->key() == Qt::Key_Delete) Unlink();
}
Ui::LinkWindow ui;
} window;
bool ProcessSentence(std::wstring& sentence, SentenceInfo sentenceInfo)
{
concurrency::reader_writer_lock::scoped_lock_read readLock(m);
auto action = separateSentences ? sentenceInfo["add sentence"] : sentenceInfo["add text"];
auto it = links.find(sentenceInfo["text number"]);
for (const auto& linkSet : { it != links.end() ? it->second : empty, sentenceInfo["text number"] > 1 ? universalLinks : empty })
for (auto link : linkSet)
((void(*)(int64_t, const wchar_t*))action)(link, sentence.c_str());
return false;
}

View File

@ -0,0 +1,47 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>LinkWindow</class>
<widget class="QDialog">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>400</width>
<height>300</height>
</rect>
</property>
<layout class="QHBoxLayout">
<item>
<widget class="QListWidget" name="linkList"/>
</item>
<item>
<layout class="QVBoxLayout">
<item>
<spacer>
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
</spacer>
</item>
<item>
<widget class="QPushButton" name="linkButton">
</widget>
</item>
<item>
<widget class="QPushButton" name="unlinkButton">
</widget>
</item>
<item>
<spacer>
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
</spacer>
</item>
</layout>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>

View File

@ -0,0 +1,212 @@
#include "qtcommon.h"
#include "extension.h"
#include "translatewrapper.h"
#include "blockmarkup.h"
#include <concurrent_priority_queue.h>
#include <fstream>
#include <QComboBox>
extern const char* NATIVE_LANGUAGE;
extern const char* TRANSLATE_TO;
extern const char* TRANSLATE_FROM;
extern const char* TRANSLATE_SELECTED_THREAD_ONLY;
extern const char* RATE_LIMIT_ALL_THREADS;
extern const char* RATE_LIMIT_SELECTED_THREAD;
extern const char* USE_TRANS_CACHE;
extern const char* FILTER_GARBAGE;
extern const char* MAX_TRANSLATIONS_IN_TIMESPAN;
extern const char* TIMESPAN;
extern const char* MAX_SENTENCE_SIZE;
extern const char* API_KEY;
extern const wchar_t* SENTENCE_TOO_LARGE_TO_TRANS;
extern const wchar_t* TRANSLATION_ERROR;
extern const wchar_t* TOO_MANY_TRANS_REQUESTS;
extern const char* TRANSLATION_PROVIDER;
extern const char* GET_API_KEY_FROM;
extern const QStringList languagesTo, languagesFrom;
extern bool translateSelectedOnly, useRateLimiter, rateLimitSelected, useCache, useFilter;
extern int tokenCount, rateLimitTimespan, maxSentenceSize;
std::pair<bool, std::wstring> Translate(const std::wstring& text, TranslationParam tlp);
QFormLayout* display;
Settings settings;
namespace
{
Synchronized<TranslationParam> tlp;
Synchronized<std::unordered_map<std::wstring, std::wstring>> translationCache;
std::string CacheFile()
{
return FormatString("%s Cache (%S).txt", TRANSLATION_PROVIDER, tlp->translateTo);
}
void SaveCache()
{
std::wstring allTranslations(L"\xfeff");
for (const auto& [sentence, translation] : translationCache.Acquire().contents)
allTranslations.append(L"|SENTENCE|").append(sentence).append(L"|TRANSLATION|").append(translation).append(L"|END|\r\n");
std::ofstream(CacheFile(), std::ios::binary | std::ios::trunc).write((const char*)allTranslations.c_str(), allTranslations.size() * sizeof(wchar_t));
}
void LoadCache()
{
translationCache->clear();
std::ifstream stream(CacheFile(), std::ios::binary);
BlockMarkupIterator savedTranslations(stream, Array<std::wstring_view>{ L"|SENTENCE|", L"|TRANSLATION|" });
auto translationCache = ::translationCache.Acquire();
while (auto read = savedTranslations.Next())
{
auto& [sentence, translation] = read.value();
translationCache->try_emplace(std::move(sentence), std::move(translation));
}
}
}
class Window : public QDialog, Localizer
{
public:
Window() : QDialog(nullptr, Qt::WindowMinMaxButtonsHint)
{
display = new QFormLayout(this);
settings.beginGroup(TRANSLATION_PROVIDER);
auto translateToCombo = new QComboBox(this);
translateToCombo->addItems(languagesTo);
int i = -1;
if (settings.contains(TRANSLATE_TO)) i = translateToCombo->findText(settings.value(TRANSLATE_TO).toString());
if (i < 0) i = translateToCombo->findText(NATIVE_LANGUAGE, Qt::MatchStartsWith);
if (i < 0) i = translateToCombo->findText("English", Qt::MatchStartsWith);
translateToCombo->setCurrentIndex(i);
SaveTranslateTo(translateToCombo->currentText());
display->addRow(TRANSLATE_TO, translateToCombo);
connect(translateToCombo, &QComboBox::currentTextChanged, this, &Window::SaveTranslateTo);
auto translateFromCombo = new QComboBox(this);
translateFromCombo->addItem("?");
translateFromCombo->addItems(languagesFrom);
i = -1;
if (settings.contains(TRANSLATE_FROM)) i = translateFromCombo->findText(settings.value(TRANSLATE_FROM).toString());
if (i < 0) i = 0;
translateFromCombo->setCurrentIndex(i);
SaveTranslateFrom(translateFromCombo->currentText());
display->addRow(TRANSLATE_FROM, translateFromCombo);
connect(translateFromCombo, &QComboBox::currentTextChanged, this, &Window::SaveTranslateFrom);
for (auto [value, label] : Array<bool&, const char*>{
{ translateSelectedOnly, TRANSLATE_SELECTED_THREAD_ONLY },
{ useRateLimiter, RATE_LIMIT_ALL_THREADS },
{ rateLimitSelected, RATE_LIMIT_SELECTED_THREAD },
{ useCache, USE_TRANS_CACHE },
{ useFilter, FILTER_GARBAGE }
})
{
value = settings.value(label, value).toBool();
auto checkBox = new QCheckBox(this);
checkBox->setChecked(value);
display->addRow(label, checkBox);
connect(checkBox, &QCheckBox::clicked, [label, &value](bool checked) { settings.setValue(label, value = checked); });
}
for (auto [value, label] : Array<int&, const char*>{
{ tokenCount, MAX_TRANSLATIONS_IN_TIMESPAN },
{ rateLimitTimespan, TIMESPAN },
{ maxSentenceSize, MAX_SENTENCE_SIZE },
})
{
value = settings.value(label, value).toInt();
auto spinBox = new QSpinBox(this);
spinBox->setRange(0, INT_MAX);
spinBox->setValue(value);
display->addRow(label, spinBox);
connect(spinBox, qOverload<int>(&QSpinBox::valueChanged), [label, &value](int newValue) { settings.setValue(label, value = newValue); });
}
if (GET_API_KEY_FROM)
{
auto keyEdit = new QLineEdit(settings.value(API_KEY).toString(), this);
tlp->authKey = S(keyEdit->text());
QObject::connect(keyEdit, &QLineEdit::textChanged, [](QString key) { settings.setValue(API_KEY, S(tlp->authKey = S(key))); });
auto keyLabel = new QLabel(QString("<a href=\"%1\">%2</a>").arg(GET_API_KEY_FROM, API_KEY), this);
keyLabel->setOpenExternalLinks(true);
display->addRow(keyLabel, keyEdit);
}
setWindowTitle(TRANSLATION_PROVIDER);
QMetaObject::invokeMethod(this, &QWidget::show, Qt::QueuedConnection);
}
~Window()
{
SaveCache();
}
private:
void SaveTranslateTo(QString language)
{
SaveCache();
settings.setValue(TRANSLATE_TO, S(tlp->translateTo = S(language)));
LoadCache();
}
void SaveTranslateFrom(QString language)
{
settings.setValue(TRANSLATE_FROM, S(tlp->translateFrom = S(language)));
}
} window;
bool ProcessSentence(std::wstring& sentence, SentenceInfo sentenceInfo)
{
if (sentenceInfo["text number"] == 0) return false;
static class
{
public:
bool Request()
{
DWORD64 current = GetTickCount64(), token;
while (tokens.try_pop(token)) if (token > current - rateLimitTimespan)
{
tokens.push(token); // popped one too many
break;
}
bool available = tokens.size() < tokenCount;
if (available) tokens.push(current);
return available;
}
private:
concurrency::concurrent_priority_queue<DWORD64, std::greater<DWORD64>> tokens;
} rateLimiter;
bool cache = false;
std::wstring translation;
if (useFilter)
{
Trim(sentence);
sentence.erase(std::remove_if(sentence.begin(), sentence.end(), [](wchar_t ch) { return ch < ' ' && ch != '\n'; }), sentence.end());
}
if (sentence.empty()) return true;
if (sentence.size() > maxSentenceSize) translation = SENTENCE_TOO_LARGE_TO_TRANS;
if (useCache)
{
auto translationCache = ::translationCache.Acquire();
if (auto it = translationCache->find(sentence); it != translationCache->end()) translation = it->second;
}
if (translation.empty() && (!translateSelectedOnly || sentenceInfo["current select"]))
if (rateLimiter.Request() || !useRateLimiter || (!rateLimitSelected && sentenceInfo["current select"])) std::tie(cache, translation) = Translate(sentence, tlp.Copy());
else translation = TOO_MANY_TRANS_REQUESTS;
if (cache) translationCache->operator[](sentence) = translation;
if (useFilter) Trim(translation);
for (int i = 0; i < translation.size(); ++i) if (translation[i] == '\r' && translation[i + 1] == '\n') translation[i] = 0x200b; // for some reason \r appears as newline - no need to double
if (translation.empty()) translation = TRANSLATION_ERROR;
(sentence += L"\x200b \n") += translation;
return true;
}
extern const std::unordered_map<std::wstring, std::wstring> codes;
TEST(
{
assert(Translate(L"こんにちは", { L"English", L"?", L"" }).second.find(L"ello") == 1 || strstr(TRANSLATION_PROVIDER, "DevTools"));
for (auto languages : { languagesFrom, languagesTo }) for (auto language : languages)
assert(codes.count(S(language)));
assert(codes.count(L"?"));
}
);

View File

@ -0,0 +1,6 @@
#pragma once
struct TranslationParam
{
std::wstring translateTo, translateFrom, authKey;
};

View File

@ -1,31 +0,0 @@
#include"plugindef.h"
bool ProcessSentence(std::wstring& sentence, SentenceInfo sentenceInfo);
extern "C" __declspec(dllexport) wchar_t* OnNewSentence(wchar_t* sentence, const InfoForExtension* sentenceInfo)
{
try
{
std::wstring sentenceCopy(sentence);
int oldSize = sentenceCopy.size();
if (ProcessSentence(sentenceCopy, SentenceInfo{ sentenceInfo }))
{
if (sentenceCopy.size() > oldSize) sentence = (wchar_t*)HeapReAlloc(GetProcessHeap(), HEAP_GENERATE_EXCEPTIONS, sentence, (sentenceCopy.size() + 1) * sizeof(wchar_t));
wcscpy_s(sentence, sentenceCopy.size() + 1, sentenceCopy.c_str());
}
}
catch (std::exception &e)
{
*sentence = L'\0';
}
return sentence;
}
bool sendclipboarddata(const std::wstring&text,HWND hwnd);
bool ProcessSentence(std::wstring& sentence, SentenceInfo sentenceInfo)
{
if (sentenceInfo["current select"] && sentenceInfo["process id"] != 0 &&sentenceInfo["toclipboard"])
{
sendclipboarddata(sentence,(HWND)sentenceInfo["HostHWND"]);
}
return false;
}

View File

@ -1,11 +1,12 @@
#include"pluginmanager.h"
#include<filesystem>
#include"Plugin/plugindef.h"
#include"Plugin/extension.h"
#include<fstream>
#include <commdlg.h>
#include"LunaHost.h"
#include"Lang/Lang.h"
#include"host.h"
typedef wchar_t* (*OnNewSentence_t)(wchar_t*, const InfoForExtension*);
std::optional<std::wstring>SelectFile(HWND hwnd,LPCWSTR lpstrFilter){
OPENFILENAME ofn;

View File

@ -1,6 +1,6 @@
#ifndef LUNA_PLUGINMANAGER_H
#define LUNA_PLUGINMANAGER_H
#include"Plugin/plugindef.h"
#include"Plugin/extension.h"
#include"textthread.h"
#include<nlohmann/json.hpp>
class LunaHost;

1
scripts/pack.bat Normal file
View File

@ -0,0 +1 @@
python pack.py

14
scripts/pack.py Normal file
View File

@ -0,0 +1,14 @@
import os, shutil
for f in os.listdir('../builds'):
if os.path.isdir('../builds/'+f)==False:continue
for dirname,_,fs in os.walk('../builds/'+f):
if dirname.endswith('translations') or dirname.endswith('translations') or dirname.endswith('imageformats') or dirname.endswith('iconengines') or dirname.endswith('bearer'):
shutil.rmtree(dirname)
continue
for ff in fs:
path=os.path.join(dirname,ff)
if ff in ['Qt5Svg.dll','libEGL.dll','libGLESv2.dll','opengl32sw.dll','D3Dcompiler_47.dll']:os.remove(path)
targetdir='../builds/'+f
target='../builds/'+f+'.zip'
os.system(rf'"C:\Program Files\7-Zip\7z.exe" a -m0=LZMA -mx9 {target} {targetdir}')