add dictionary function to extra window, store colors as argb hex, decrease memory usage of replacer

This commit is contained in:
Akash Mozumdar 2019-09-05 13:42:30 -04:00
parent 9415e83511
commit b7c9f0bfce
6 changed files with 287 additions and 124 deletions

View File

@ -2,6 +2,9 @@
#include "extension.h"
#include "ui_extrawindow.h"
#include "defs.h"
#include <fstream>
#include <filesystem>
#include <process.h>
#include <QColorDialog>
#include <QFontDialog>
#include <QMenu>
@ -14,66 +17,43 @@ extern const char* TOPMOST;
extern const char* SHOW_ORIGINAL;
extern const char* SHOW_ORIGINAL_INFO;
extern const char* SIZE_LOCK;
extern const char* DICTIONARY;
extern const char* DICTIONARY_INSTRUCTIONS;
extern const char* BG_COLOR;
extern const char* TEXT_COLOR;
extern const char* FONT;
extern const char* SAVE_SETTINGS;
struct Window : QDialog
constexpr auto DICTIONARY_SAVE_FILE = u8"SavedDictionary.txt";
struct PrettyWindow : QDialog
{
public:
Window()
PrettyWindow(const char* name)
{
ui.setupUi(this);
settings.beginGroup("Extra Window");
setWindowFlags(Qt::FramelessWindowHint);
setAttribute(Qt::WA_TranslucentBackground);
QMetaObject::invokeMethod(this, [this]
{
show();
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, palette().window().color()).value<QColor>());
setTextColor(settings.value(TEXT_COLOR, ui.display->palette().windowText().color()).value<QColor>());
setLock(settings.value(SIZE_LOCK, false).toBool());
setTopmost(settings.value(TOPMOST, false).toBool());
setGeometry(settings.value(WINDOW, geometry()).toRect());
menu.addAction(FONT, this, &Window::RequestFont);
menu.addAction(BG_COLOR, [this] { setBackgroundColor(QColorDialog::getColor(bgColor, this, BG_COLOR, QColorDialog::ShowAlphaChannel)); });
menu.addAction(TEXT_COLOR, [this] { setTextColor(QColorDialog::getColor(ui.display->palette().windowText().color(), this, TEXT_COLOR, QColorDialog::ShowAlphaChannel)); });
for (auto [name, default, slot] : Array<std::tuple<const char*, bool, void(Window::*)(bool)>>{
{ TOPMOST, false, &Window::setTopmost },
{ SIZE_LOCK, false, &Window::setLock },
{ SHOW_ORIGINAL, true, &Window::setShowOriginal }
})
{
auto action = menu.addAction(name, this, slot);
action->setCheckable(true);
action->setChecked(settings.value(name, default).toBool());
}
setBgColor(settings.value(BG_COLOR, bgColor).value<QColor>());
setTextColor(settings.value(TEXT_COLOR, textColor()).value<QColor>());
menu.addAction(FONT, this, &PrettyWindow::RequestFont);
menu.addAction(BG_COLOR, [this] { setBgColor(QColorDialog::getColor(bgColor, this, BG_COLOR, QColorDialog::ShowAlphaChannel)); });
menu.addAction(TEXT_COLOR, [this] { setTextColor(QColorDialog::getColor(textColor(), this, TEXT_COLOR, QColorDialog::ShowAlphaChannel)); });
connect(ui.display, &QLabel::customContextMenuRequested, [this](QPoint point) { menu.exec(mapToGlobal(point)); });
QMetaObject::invokeMethod(this, [this] { AddSentence(EXTRA_WINDOW_INFO); }, Qt::QueuedConnection);
}, Qt::QueuedConnection);
}
~Window()
~PrettyWindow()
{
settings.setValue(WINDOW, geometry());
settings.sync();
}
void AddSentence(const QString& sentence)
{
sentenceHistory.push_back(sentence);
i = sentenceHistory.size() - 1;
ui.display->setText(sentence);
}
Ui::ExtraWindow ui;
protected:
QMenu menu{ ui.display };
QSettings settings{ CONFIG_FILE, QSettings::IniFormat, this };
private:
@ -87,41 +67,25 @@ private:
}
};
void setBackgroundColor(QColor color)
void setBgColor(QColor color)
{
if (!color.isValid()) return;
if (color.alpha() == 0) color.setAlpha(1);
bgColor = color;
repaint();
settings.setValue(BG_COLOR, color);
settings.setValue(BG_COLOR, "#" + QString::number(color.rgba(), 16));
};
QColor textColor()
{
return ui.display->palette().color(QPalette::WindowText);
}
void setTextColor(QColor color)
{
if (!color.isValid()) return;
auto newPalette = ui.display->palette();
newPalette.setColor(QPalette::WindowText, color);
ui.display->setPalette(newPalette);
settings.setValue(TEXT_COLOR, color);
};
void setTopmost(bool topmost)
{
SetWindowPos((HWND)winId(), topmost ? HWND_TOPMOST : HWND_NOTOPMOST, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE);
settings.setValue(TOPMOST, topmost);
};
void setLock(bool lock)
{
locked = lock;
setSizeGripEnabled(!lock);
settings.setValue(SIZE_LOCK, lock);
};
void setShowOriginal(bool showOriginal)
{
if (!showOriginal) QMessageBox::information(this, SHOW_ORIGINAL, SHOW_ORIGINAL_INFO);
settings.setValue(SHOW_ORIGINAL, showOriginal);
ui.display->setPalette(QPalette(color, {}, {}, {}, {}, {}, {}));
settings.setValue(TEXT_COLOR, "#" + QString::number(color.rgba(), 16));
};
void paintEvent(QPaintEvent*) override
@ -129,40 +93,219 @@ private:
QPainter(this).fillRect(rect(), bgColor);
}
QColor bgColor{ palette().window().color() };
};
class ExtraWindow : public PrettyWindow
{
public:
ExtraWindow() :
PrettyWindow("Extra Window")
{
setGeometry(settings.value(WINDOW, geometry()).toRect());
for (auto [name, default, slot] : Array<std::tuple<const char*, bool, void(ExtraWindow::*)(bool)>>{
{ TOPMOST, false, &ExtraWindow::setTopmost },
{ SIZE_LOCK, false, &ExtraWindow::setLock },
{ SHOW_ORIGINAL, true, &ExtraWindow::setShowOriginal },
{ 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);
}
ui.display->installEventFilter(this);
QMetaObject::invokeMethod(this, [this]
{
show();
QMetaObject::invokeMethod(this, [this] { AddSentence(EXTRA_WINDOW_INFO); }, Qt::QueuedConnection);
}, Qt::QueuedConnection);
}
~ExtraWindow()
{
settings.setValue(WINDOW, geometry());
}
void AddSentence(QString sentence)
{
if (!showOriginal) sentence = sentence.section('\n', sentence.count('\n') / 2 + 1);
sentenceHistory.push_back(sentence);
historyIndex = sentenceHistory.size() - 1;
ui.display->setText(sentence);
}
private:
void setTopmost(bool topmost)
{
SetWindowPos((HWND)winId(), topmost ? HWND_TOPMOST : HWND_NOTOPMOST, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE);
settings.setValue(TOPMOST, topmost);
};
void setLock(bool locked)
{
setSizeGripEnabled(!locked);
settings.setValue(SIZE_LOCK, this->locked = locked);
};
void setShowOriginal(bool showOriginal)
{
if (!showOriginal && settings.value(SHOW_ORIGINAL, false).toBool()) QMessageBox::information(this, SHOW_ORIGINAL, SHOW_ORIGINAL_INFO);
settings.setValue(SHOW_ORIGINAL, this->showOriginal = showOriginal);
};
void setUseDictionary(bool useDictionary)
{
if (useDictionary)
{
dictionaryWindow.UpdateDictionary();
if (dictionaryWindow.dictionary.empty())
{
std::ofstream(DICTIONARY_SAVE_FILE) << DICTIONARY_INSTRUCTIONS;
_spawnlp(_P_DETACH, "notepad", "notepad", DICTIONARY_SAVE_FILE, NULL); // show file to user
}
}
settings.setValue(DICTIONARY, this->useDictionary = useDictionary);
}
bool eventFilter(QObject*, QEvent* event) override
{
if (useDictionary && event->type() == QEvent::MouseButtonRelease && ui.display->hasSelectedText())
{
dictionaryWindow.ui.display->setFixedWidth(ui.display->width());
dictionaryWindow.setTerm(ui.display->text().mid(ui.display->selectionStart()));
dictionaryWindow.move({ x(), y() - dictionaryWindow.height() });
}
if (event->type() == QEvent::MouseButtonPress) dictionaryWindow.hide();
return false;
}
void mousePressEvent(QMouseEvent* event) override
{
dictionaryWindow.hide();
oldPos = event->globalPos();
}
void mouseMoveEvent(QMouseEvent* event) override
{
const QPoint delta = event->globalPos() - oldPos;
if (!locked) move(x() + delta.x(), y() + delta.y());
if (!locked) move(pos() + event->globalPos() - oldPos);
oldPos = event->globalPos();
}
void wheelEvent(QWheelEvent* event) override
{
QPoint scroll = event->angleDelta();
if (scroll.y() > 0 && i > 0) ui.display->setText(sentenceHistory[--i]);
if (scroll.y() < 0 && i + 1 < sentenceHistory.size()) ui.display->setText(sentenceHistory[++i]);
int scroll = event->angleDelta().y();
if (scroll > 0 && historyIndex > 0) ui.display->setText(sentenceHistory[--historyIndex]);
if (scroll < 0 && historyIndex + 1 < sentenceHistory.size()) ui.display->setText(sentenceHistory[++historyIndex]);
}
QMenu menu{ ui.display };
bool locked = true;
QColor bgColor;
bool locked, showOriginal, useDictionary;
QPoint oldPos;
std::vector<QString> sentenceHistory;
int i = 0;
} window;
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();
owningStorage.clear();
auto StoreCopy = [&](const std::string& string)
{
return &*owningStorage.insert(owningStorage.end(), string.c_str(), string.c_str() + string.size() + 1);
};
std::string savedDictionary(std::istreambuf_iterator(std::ifstream(DICTIONARY_SAVE_FILE)), {});
owningStorage.reserve(savedDictionary.size());
for (size_t end = 0; ;)
{
size_t term = savedDictionary.find("|TERM|", end);
size_t definition = savedDictionary.find("|DEFINITION|", term);
if ((end = savedDictionary.find("|END|", definition)) == std::string::npos) break;
auto storedDefinition = StoreCopy(savedDictionary.substr(definition + 12, end - definition - 12));
for (size_t next; (next = savedDictionary.find("|TERM|", term + 1)) != std::string::npos && next < definition; term = next)
dictionary.push_back({ StoreCopy(savedDictionary.substr(term + 6, next - term - 6)), storedDefinition });
dictionary.push_back({ StoreCopy(savedDictionary.substr(term + 6, definition - term - 6)), storedDefinition });
}
auto oldData = owningStorage.data();
owningStorage.shrink_to_fit();
dictionary.shrink_to_fit();
for (auto& [term, definition] : dictionary)
{
term += owningStorage.data() - oldData;
definition += owningStorage.data() - oldData;
}
std::sort(dictionary.begin(), dictionary.end());
}
void setTerm(QString term)
{
UpdateDictionary();
definitions.clear();
definitionIndex = 0;
for (QByteArray utf8term = term.left(200).toUtf8(); !utf8term.isEmpty(); utf8term.chop(1))
for (auto [it, end] = std::equal_range(dictionary.begin(), dictionary.end(), DictionaryEntry{ utf8term }); it != end; ++it)
definitions.push_back(QStringLiteral("<h3>%1 (%3/%4)</h3>%2").arg(utf8term, it->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;
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;
private:
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());
}
std::filesystem::file_time_type dictionaryFileLastWrite;
std::vector<char> owningStorage;
std::vector<QString> definitions;
int definitionIndex;
} dictionaryWindow;
} extraWindow;
bool ProcessSentence(std::wstring& sentence, SentenceInfo sentenceInfo)
{
if (!sentenceInfo["current select"]) return false;
QString qSentence = S(sentence);
if (!window.settings.value(SHOW_ORIGINAL, true).toBool()) qSentence = qSentence.section('\n', qSentence.count('\n') / 2 + 1);
QMetaObject::invokeMethod(&window, [=] { window.AddSentence(qSentence); });
if (sentenceInfo["current select"]) QMetaObject::invokeMethod(&extraWindow, [sentence = S(sentence)] { extraWindow.AddSentence(sentence); });
return false;
}

View File

@ -10,9 +10,6 @@
<height>300</height>
</rect>
</property>
<property name="sizeGripEnabled">
<bool>true</bool>
</property>
<layout class="QHBoxLayout">
<item>
<widget class="QLabel" name="display">

View File

@ -74,13 +74,12 @@ private:
void Save()
{
auto script = scriptEditor.toPlainText().toUtf8();
std::ofstream(LUA_SAVE_FILE, std::ios::out | std::ios::trunc).write(script, script.size());
QTextFile(LUA_SAVE_FILE, QIODevice::WriteOnly | QIODevice::Truncate).write(scriptEditor.toPlainText().toUtf8());
}
QWidget centralWidget{ this };
QHBoxLayout layout{ &centralWidget };
QPlainTextEdit scriptEditor{ S(std::string(std::istreambuf_iterator<char>(std::ifstream(LUA_SAVE_FILE, std::ios::in)), {})), &centralWidget };
QPlainTextEdit scriptEditor{ QTextFile(LUA_SAVE_FILE, QIODevice::ReadOnly).readAll(), &centralWidget };
QPushButton loadButton{ LOAD_LUA_SCRIPT, &centralWidget };
} window;

View File

@ -14,16 +14,14 @@ std::shared_mutex m;
class Trie
{
public:
Trie(const std::unordered_map<std::wstring, std::wstring>& replacements)
Trie(std::unordered_map<std::wstring, std::wstring> replacements)
{
for (const auto& [original, replacement] : replacements)
{
Node* current = &root;
for (auto ch : original)
if (Ignore(ch));
else if (auto& next = current->next[ch]) current = next.get();
else current = (next = std::make_unique<Node>()).get();
if (current != &root) current->value = replacement;
for (auto ch : original) if (!Ignore(ch)) current = Next(current, ch);
if (current != &root)
current->value = owningStorage.insert(owningStorage.end(), replacement.c_str(), replacement.c_str() + replacement.size() + 1) - owningStorage.begin();
}
}
@ -32,20 +30,18 @@ public:
std::wstring result;
for (int i = 0; i < sentence.size();)
{
std::wstring replacement(1, sentence[i]);
std::wstring_view replacement(sentence.c_str() + i, 1);
int originalLength = 1;
const Node* current = &root;
for (int j = i; j < sentence.size() + 1; ++j)
for (int j = i; current && j <= sentence.size(); ++j)
{
if (current->value)
if (current->value >= 0)
{
replacement = current->value.value();
replacement = owningStorage.data() + current->value;
originalLength = j - i;
}
if (current->next.count(sentence[j]) > 0) current = current->next.at(sentence[j]).get();
else if (Ignore(sentence[j]));
else break;
if (!Ignore(sentence[j])) current = Next(current, sentence[j]);
}
result += replacement;
@ -54,43 +50,70 @@ public:
return result;
}
bool Empty()
{
return root.charMap.empty();
}
private:
static bool Ignore(wchar_t ch)
{
return ch <= 0x20 || std::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::unordered_map<wchar_t, std::unique_ptr<Node>, Identity<wchar_t>> next;
std::optional<std::wstring> value;
std::vector<std::pair<wchar_t, std::unique_ptr<Node>>> charMap;
ptrdiff_t value = -1;
} root;
std::vector<wchar_t> owningStorage;
} trie = { {} };
std::unordered_map<std::wstring, std::wstring> Parse(const std::wstring& replacementScript)
std::unordered_map<std::wstring, std::wstring> Parse(std::wstring_view replacementScript)
{
std::unordered_map<std::wstring, std::wstring> replacements;
size_t end = 0;
while (true)
for (size_t end = 0; ;)
{
size_t original = replacementScript.find(L"|ORIG|", end);
size_t becomes = replacementScript.find(L"|BECOMES|", original);
if ((end = replacementScript.find(L"|END|", becomes)) == std::wstring::npos) break;
replacements[replacementScript.substr(original + 6, becomes - original - 6)] = replacementScript.substr(becomes + 9, end - becomes - 9);
replacements[std::wstring(replacementScript.substr(original + 6, becomes - original - 6))] = replacementScript.substr(becomes + 9, end - becomes - 9);
}
return 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::vector<BYTE> file(std::istreambuf_iterator(std::ifstream(REPLACE_SAVE_FILE, std::ios::binary)), {});
std::scoped_lock l(m);
trie = Trie(Parse({ (wchar_t*)file.data(), file.size() / sizeof(wchar_t) }));
}
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:
{
std::vector<BYTE> file(std::istreambuf_iterator<char>(std::ifstream(REPLACE_SAVE_FILE, std::ios::in | std::ios::binary)), {});
if (Parse(std::wstring((wchar_t*)file.data(), file.size() / sizeof(wchar_t))).empty())
UpdateReplacements();
if (trie.Empty())
{
std::ofstream(REPLACE_SAVE_FILE, std::ios::out | std::ios::binary | std::ios::trunc).write((char*)REPLACER_INSTRUCTIONS, wcslen(REPLACER_INSTRUCTIONS) * sizeof(wchar_t));
std::ofstream(REPLACE_SAVE_FILE, std::ios::binary).write((char*)REPLACER_INSTRUCTIONS, wcslen(REPLACER_INSTRUCTIONS) * sizeof(wchar_t));
_spawnlp(_P_DETACH, "notepad", "notepad", REPLACE_SAVE_FILE, NULL); // show file to user
}
}
@ -105,17 +128,7 @@ BOOL WINAPI DllMain(HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved
bool ProcessSentence(std::wstring& sentence, SentenceInfo)
{
try
{
static_assert(std::has_unique_object_representations_v<decltype(replaceFileLastWrite)::value_type>);
if (replaceFileLastWrite.exchange(std::filesystem::last_write_time(REPLACE_SAVE_FILE)) != std::filesystem::last_write_time(REPLACE_SAVE_FILE))
{
std::scoped_lock l(m);
std::vector<BYTE> file(std::istreambuf_iterator<char>(std::ifstream(REPLACE_SAVE_FILE, std::ios::in | std::ios::binary)), {});
trie = Trie(Parse(std::wstring((wchar_t*)file.data(), file.size() / sizeof(wchar_t))));
}
}
catch (std::filesystem::filesystem_error) {}
UpdateReplacements();
std::shared_lock l(m);
sentence = trie.Replace(sentence);
@ -132,7 +145,7 @@ And this text ツ  
assert(replacements.size() == 4);
std::wstring original = LR"(Don't replace this 
delete this)";
std::wstring replaced = Trie(replacements).Replace(original);
std::wstring replaced = Trie(std::move(replacements)).Replace(original);
assert(replaced == L"Don't replace thisgoodbye idiot hello");
}
);

View File

@ -119,6 +119,17 @@ const wchar_t* TRANSLATION_ERROR = L"Error while translating";
const char* EXTRA_WINDOW_INFO = u8R"(Right click to change settings
Click and drag on window edges to move, or the bottom right corner to resize)";
const char* TOPMOST = u8"Always on top";
const char* DICTIONARY = u8"Dictionary";
const char* DICTIONARY_INSTRUCTIONS = u8R"(This file is used only for the "Dictionary" feature of the Extra Window extension.
It is not meant to be written manually (though it can be).
You should look for a dictionary in this format online (https://artikash.github.io/?dictionary is a good place to start).
Alternatively, if you're a programmer, you can write a script to convert a dictionary from another format with the info below.
Once you have a dictionary, to look up some text in Extra Window, select it. All matching definitions will be shown.
Definitions are formatted like this:|TERM|Hola|TERM|hola|TERM|Bonjour|TERM|bonjour|DEFINITION|hello|END|
The definition can include rich text (https://doc.qt.io/qt-5/richtext-html-subset.html) which will be formatted properly.
All text in this file outside of definitions is ignored.
Terms longer than 50 characters may not be shown (for performance reasons that should be fixed soon).
This file must be encoded in UTF-8.)";
const char* SHOW_ORIGINAL = u8"Original text";
const char* SHOW_ORIGINAL_INFO = u8R"(Original text will not be shown
Only works if this extension is used directly after a translation extension)";

View File

@ -206,7 +206,7 @@ void SearchForHooks(SearchParam spUser)
{
VirtualQuery((void*)moduleStopAddress, &info, sizeof(info));
moduleStopAddress = (uintptr_t)info.BaseAddress + info.RegionSize;
} while (info.Protect > PAGE_EXECUTE);
} while (info.Protect >= PAGE_EXECUTE);
moduleStopAddress -= info.RegionSize;
ConsoleOutput(STARTING_SEARCH);