Add configurable offline layout correction and latest Windows release
This commit is contained in:
+103
-9
@@ -314,6 +314,11 @@ void App::applySettings(const Settings &s)
|
||||
{
|
||||
bool autostartChanged = false;
|
||||
validateSettings(s);
|
||||
if (s.autoCorrect)
|
||||
{
|
||||
AutoCorrector probe;
|
||||
probe.configure(std::filesystem::path(executable()).parent_path() / L"dictionaries", s.autoLanguages);
|
||||
}
|
||||
if (!registerHotkeys(s))
|
||||
{
|
||||
registerHotkeys(settings, true);
|
||||
@@ -348,6 +353,8 @@ void App::applySettings(const Settings &s)
|
||||
last.reset();
|
||||
++inputVersion;
|
||||
}
|
||||
// Clear on the worker so the next check reloads edits to dictionary files.
|
||||
enqueue([this] { autoCorrector.configure({}, {}); });
|
||||
SetThreadExecutionState(ES_CONTINUOUS |
|
||||
(settings.awake ? (ES_DISPLAY_REQUIRED | ES_SYSTEM_REQUIRED) : 0));
|
||||
}
|
||||
@@ -435,6 +442,9 @@ void App::keyboard(const KBDLLHOOKSTRUCT &k)
|
||||
return;
|
||||
}
|
||||
run.record(ch, fg, layout, GetTickCount64());
|
||||
if (settings.autoCorrect && ch == L' ' && run.keys.size() >= (size_t)settings.autoMinLength + 1 &&
|
||||
run.keys.size() <= 41 && !busy && !run.overflow)
|
||||
PostMessageW(window, AutoCorrectMessage, (WPARAM)inputVersion.load(), 0);
|
||||
if (busy || settings.snippets.empty())
|
||||
return;
|
||||
auto keys = lower(run.keys);
|
||||
@@ -611,6 +621,8 @@ void App::transform(int command, int snippet)
|
||||
{ return GetForegroundWindow() == target && inputVersion.load() == version; };
|
||||
for (int i = 0; i < 8 && !modifiersReleased(); ++i)
|
||||
Sleep(10);
|
||||
if (!modifiersReleased() && command == AutoCorrect)
|
||||
return;
|
||||
if (!modifiersReleased())
|
||||
releaseModifiers();
|
||||
if (!valid())
|
||||
@@ -624,9 +636,24 @@ void App::transform(int command, int snippet)
|
||||
return;
|
||||
if (to >= 0 && !available[to].convertible)
|
||||
{
|
||||
PostMessageW(target, WM_INPUTLANGCHANGEREQUEST, 0, (LPARAM)available[to].handle);
|
||||
switchWindowLayout(target, available[to].handle, valid);
|
||||
return;
|
||||
}
|
||||
if (command == AutoCorrect)
|
||||
{
|
||||
if (!config.autoCorrect || recent.window != target || recent.overflow ||
|
||||
recent.keys.empty() || recent.keys.back() != L' ' || hint < 0)
|
||||
return;
|
||||
autoCorrector.configure(std::filesystem::path(executable()).parent_path() /
|
||||
L"dictionaries",
|
||||
config.autoLanguages);
|
||||
std::wstring predicted;
|
||||
for (wchar_t key : recent.keys)
|
||||
predicted += key == L' ' ? L' ' : available[hint].at(physicalLayout().code(key));
|
||||
if (!autoCorrector.suggest(predicted, hint, available, config) ||
|
||||
!autoCorrectionAllowed(target, config) || !valid())
|
||||
return;
|
||||
}
|
||||
Transaction tx(*this, target, version);
|
||||
log(L"Transform target=" + processName(target) + L" command=" + std::to_wstring(command) +
|
||||
L" recent=" + std::to_wstring(config.recent) + L" keys=" +
|
||||
@@ -634,7 +661,36 @@ void App::transform(int command, int snippet)
|
||||
std::to_wstring(recent.window == target));
|
||||
std::wstring text, result;
|
||||
bool automatic = false;
|
||||
if (command == Undo)
|
||||
if (command == AutoCorrect)
|
||||
{
|
||||
// The editor must select the complete previous word. Unlike manual
|
||||
// conversion, automatic correction must never fall back to a suffix.
|
||||
if (!tx.valid() || !chord(VK_LEFT, MOD_CONTROL | MOD_SHIFT))
|
||||
return;
|
||||
Sleep(25);
|
||||
text = tx.copy();
|
||||
// Some editors first select only ") ". Extend by whole-word
|
||||
// boundaries, then still verify against the complete tracked run.
|
||||
for (int i = 0; i < 3 && autoClosingSelection(text, config) && tx.valid(); ++i)
|
||||
{
|
||||
if (!chord(VK_LEFT, MOD_CONTROL | MOD_SHIFT))
|
||||
break;
|
||||
Sleep(25);
|
||||
text = tx.copy();
|
||||
}
|
||||
hint = matchAutoInput(recent, text, available, physicalLayout(), config);
|
||||
auto suggestion = autoCorrector.suggest(text, hint, available, config);
|
||||
if (!suggestion || !tx.valid() || !autoCorrectionAllowed(target, config))
|
||||
{
|
||||
if (tx.valid())
|
||||
chord(VK_RIGHT);
|
||||
return;
|
||||
}
|
||||
to = suggestion->target;
|
||||
result = suggestion->text;
|
||||
automatic = true;
|
||||
}
|
||||
else if (command == Undo)
|
||||
{
|
||||
if (!previous || previous->window != target || previous->version != version ||
|
||||
GetTickCount64() - previous->at > 120000)
|
||||
@@ -649,6 +705,10 @@ void App::transform(int command, int snippet)
|
||||
return;
|
||||
}
|
||||
result = previous->original;
|
||||
if (previous->autoCorrected)
|
||||
{
|
||||
to = previous->sourceLayout;
|
||||
}
|
||||
automatic = true;
|
||||
}
|
||||
else if (snippet >= 0)
|
||||
@@ -710,10 +770,10 @@ void App::transform(int command, int snippet)
|
||||
if (text.empty())
|
||||
{
|
||||
if (to >= 0 && config.switchLayout)
|
||||
PostMessageW(target, WM_INPUTLANGCHANGEREQUEST, 0, (LPARAM)available[to].handle);
|
||||
switchWindowLayout(target, available[to].handle, valid);
|
||||
return;
|
||||
}
|
||||
if (command != Undo && snippet < 0)
|
||||
if (command != Undo && command != AutoCorrect && snippet < 0)
|
||||
{
|
||||
if (command == Case)
|
||||
result = cycleCase(text);
|
||||
@@ -750,6 +810,8 @@ void App::transform(int command, int snippet)
|
||||
sent = tx.paste(textClip(result));
|
||||
else if (automatic && tx.valid())
|
||||
chord(VK_RIGHT);
|
||||
if (sent && command == Undo && previous && previous->autoCorrected)
|
||||
autoCorrector.reject(previous->original, config);
|
||||
if (tx.valid() && (sent || automatic))
|
||||
{
|
||||
std::lock_guard lock(stateMutex);
|
||||
@@ -757,8 +819,8 @@ void App::transform(int command, int snippet)
|
||||
if (command == Undo || snippet >= 0)
|
||||
last.reset();
|
||||
else
|
||||
last = LastTransform{target, text, result, to >= 0 ? to : hint,
|
||||
GetTickCount64(), version};
|
||||
last = LastTransform{target, text, result, to >= 0 ? to : hint,
|
||||
GetTickCount64(), version, hint, command == AutoCorrect};
|
||||
}
|
||||
if (snippet >= 0 && sent && tx.valid())
|
||||
{
|
||||
@@ -770,8 +832,11 @@ void App::transform(int command, int snippet)
|
||||
chord(newlines(suffix) == newlines(t.suffix) ? VK_LEFT : VK_RIGHT);
|
||||
}
|
||||
}
|
||||
if (to >= 0 && config.switchLayout && tx.valid())
|
||||
PostMessageW(target, WM_INPUTLANGCHANGEREQUEST, 0, (LPARAM)available[to].handle);
|
||||
if (to >= 0 && config.switchLayout && tx.valid() && (sent || result == text))
|
||||
{
|
||||
bool switched = switchWindowLayout(target, available[to].handle, valid);
|
||||
log(L"Layout switch confirmed=" + std::to_wstring(switched));
|
||||
}
|
||||
log(L"Text operation completed in " + std::to_wstring(GetTickCount64() - start) + L" ms");
|
||||
}
|
||||
catch (const std::exception &e)
|
||||
@@ -1002,12 +1067,20 @@ LRESULT CALLBACK App::windowProc(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp)
|
||||
case TrayMessage:
|
||||
if (LOWORD(lp) == WM_CONTEXTMENU)
|
||||
app->trayMenu();
|
||||
else if (LOWORD(lp) == WM_LBUTTONDBLCLK || LOWORD(lp) == NIN_KEYSELECT)
|
||||
else if (LOWORD(lp) == WM_LBUTTONDBLCLK)
|
||||
// The shell still owns the mouse gesture on the second button-down.
|
||||
// Activate after release, so the tray cannot take focus back on button-up.
|
||||
SetTimer(hwnd, 4, 50, nullptr);
|
||||
else if (LOWORD(lp) == NIN_KEYSELECT)
|
||||
app->dispatch(20); // Settings is the tray's default action.
|
||||
return 0;
|
||||
case WM_CLIPBOARDUPDATE:
|
||||
app->captureHistory();
|
||||
return 0;
|
||||
case AutoCorrectMessage:
|
||||
app->pendingAutoVersion = (uint64_t)wp;
|
||||
SetTimer(hwnd, 3, 250, nullptr);
|
||||
return 0;
|
||||
case SnippetMessage:
|
||||
app->transform(0, (int)wp);
|
||||
return 0;
|
||||
@@ -1040,6 +1113,21 @@ LRESULT CALLBACK App::windowProc(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp)
|
||||
return TRUE;
|
||||
}
|
||||
case WM_TIMER:
|
||||
if (wp == 4)
|
||||
{
|
||||
if (GetAsyncKeyState(VK_LBUTTON) & 0x8000)
|
||||
return 0;
|
||||
KillTimer(hwnd, 4);
|
||||
app->dispatch(20);
|
||||
return 0;
|
||||
}
|
||||
if (wp == 3)
|
||||
{
|
||||
KillTimer(hwnd, 3);
|
||||
if (app->settings.autoCorrect && app->inputVersion.load() == app->pendingAutoVersion)
|
||||
app->transform(AutoCorrect);
|
||||
return 0;
|
||||
}
|
||||
if (wp == 2)
|
||||
{
|
||||
PostMessageW(hwnd, WM_CLOSE, 0, 0);
|
||||
@@ -1127,6 +1215,12 @@ int App::execute()
|
||||
MSG msg;
|
||||
while (GetMessageW(&msg, nullptr, 0, 0) > 0)
|
||||
{
|
||||
if (settingsWindow && !IsWindowEnabled(settingsWindow))
|
||||
{
|
||||
HWND popup = GetLastActivePopup(settingsWindow);
|
||||
if (popup != settingsWindow && IsDialogMessageW(popup, &msg))
|
||||
continue;
|
||||
}
|
||||
if (settingsWindow && IsDialogMessageW(settingsWindow, &msg))
|
||||
continue;
|
||||
if (snippetsWindow && IsDialogMessageW(snippetsWindow, &msg))
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#pragma once
|
||||
#include "autocorrect.h"
|
||||
#include "storage.h"
|
||||
#include <atomic>
|
||||
#include <condition_variable>
|
||||
@@ -11,7 +12,9 @@ namespace ks
|
||||
{
|
||||
inline constexpr UINT TrayMessage = WM_APP + 1, ErrorMessage = WM_APP + 2, HistoryMessage = WM_APP + 3,
|
||||
SnippetMessage = WM_APP + 4;
|
||||
inline constexpr int Toggle = 1, Case = 2, Translit = 3, Undo = 4, ShowHistory = 5, Direct = 100;
|
||||
inline constexpr int Toggle = 1, Case = 2, Translit = 3, Undo = 4, ShowHistory = 5, AutoCorrect = 6,
|
||||
Direct = 100;
|
||||
inline constexpr UINT AutoCorrectMessage = WM_APP + 6;
|
||||
inline constexpr UINT CaptureHotkeyMessage = WM_APP + 5;
|
||||
struct LastTransform
|
||||
{
|
||||
@@ -20,6 +23,8 @@ struct LastTransform
|
||||
int layout = -1;
|
||||
ULONGLONG at{};
|
||||
uint64_t version{};
|
||||
int sourceLayout = -1;
|
||||
bool autoCorrected = false;
|
||||
};
|
||||
class App
|
||||
{
|
||||
@@ -30,6 +35,7 @@ class App
|
||||
std::vector<Layout> layouts;
|
||||
Settings settings;
|
||||
std::unique_ptr<History> history;
|
||||
AutoCorrector autoCorrector;
|
||||
std::atomic<bool> busy{false}, suspended{false};
|
||||
std::atomic<DWORD> ownSequence{0};
|
||||
std::atomic<uint64_t> inputVersion{0};
|
||||
@@ -70,6 +76,7 @@ class App
|
||||
std::thread worker;
|
||||
bool stop{};
|
||||
UINT taskbarCreated{};
|
||||
uint64_t pendingAutoVersion{};
|
||||
std::vector<std::pair<int, Hotkey>> hookBindings;
|
||||
HotkeyHook hotkeyHook;
|
||||
static LRESULT CALLBACK windowProc(HWND, UINT, WPARAM, LPARAM);
|
||||
|
||||
@@ -0,0 +1,241 @@
|
||||
#include "autocorrect.h"
|
||||
#include <algorithm>
|
||||
#include <charconv>
|
||||
#include <uiautomation.h>
|
||||
namespace ks
|
||||
{
|
||||
static bool openingDelimiter(wchar_t c, const Settings &settings)
|
||||
{
|
||||
return settings.autoPrefixChars.find(c) != std::wstring::npos;
|
||||
}
|
||||
static bool trailingDelimiter(wchar_t c, const Settings &settings)
|
||||
{
|
||||
return c == L' ' || settings.autoSuffixChars.find(c) != std::wstring::npos;
|
||||
}
|
||||
bool autoClosingSelection(const std::wstring &text, const Settings &settings)
|
||||
{
|
||||
return !text.empty() && text.size() <= 6 &&
|
||||
std::all_of(text.begin(), text.end(), [&](wchar_t c) { return trailingDelimiter(c, settings); }) &&
|
||||
text.find_first_not_of(L' ') != text.npos;
|
||||
}
|
||||
std::wstring layoutLanguage(const Layout &layout)
|
||||
{
|
||||
if (layout.id == L"en" || layout.id == L"uk" || layout.id == L"ru")
|
||||
return layout.id;
|
||||
switch (PRIMARYLANGID(LOWORD((uintptr_t)layout.handle)))
|
||||
{
|
||||
case LANG_ENGLISH:
|
||||
return L"en";
|
||||
case LANG_UKRAINIAN:
|
||||
return L"uk";
|
||||
case LANG_RUSSIAN:
|
||||
return L"ru";
|
||||
default:
|
||||
return {};
|
||||
}
|
||||
}
|
||||
bool safeAutoWord(const std::wstring &word, int minimumLength)
|
||||
{
|
||||
if (minimumLength < 1 || minimumLength > 32 || word.size() < (size_t)minimumLength || word.size() > 32 ||
|
||||
!std::all_of(word.begin(), word.end(), letter))
|
||||
return false;
|
||||
auto small = lower(word);
|
||||
return word == small || (word.substr(1) == small.substr(1) && word != upper(word));
|
||||
}
|
||||
std::string_view AutoCorrector::Dictionary::word(size_t index) const
|
||||
{
|
||||
auto start = offsets[index];
|
||||
auto end = bytes.find('\t', start);
|
||||
return std::string_view(bytes).substr(start, end - start);
|
||||
}
|
||||
AutoCorrector::Dictionary::Dictionary(const std::filesystem::path &path)
|
||||
{
|
||||
auto raw = readFile(path, 2 * 1024 * 1024);
|
||||
bytes.assign(raw.begin(), raw.end());
|
||||
for (size_t start = 0; start < bytes.size();)
|
||||
{
|
||||
auto end = bytes.find('\n', start);
|
||||
if (end == bytes.npos)
|
||||
end = bytes.size();
|
||||
auto tab = bytes.find('\t', start);
|
||||
if (tab == bytes.npos || tab >= end || tab == start || tab - start > 160)
|
||||
throw std::runtime_error("Invalid auto-correction dictionary");
|
||||
int rank{};
|
||||
auto rankEnd = end > tab + 1 && bytes[end - 1] == '\r' ? end - 1 : end;
|
||||
auto parse = std::from_chars(bytes.data() + tab + 1, bytes.data() + rankEnd, rank);
|
||||
if (parse.ec != std::errc() || parse.ptr != bytes.data() + rankEnd || rank <= 0)
|
||||
throw std::runtime_error("Invalid dictionary rank");
|
||||
offsets.push_back((uint32_t)start);
|
||||
start = end + 1;
|
||||
}
|
||||
if (offsets.empty())
|
||||
throw std::runtime_error("Empty auto-correction dictionary");
|
||||
for (size_t i = 1; i < offsets.size(); ++i)
|
||||
if (word(i - 1) >= word(i))
|
||||
throw std::runtime_error("Dictionary must be uniquely sorted");
|
||||
offsets.shrink_to_fit();
|
||||
bytes.shrink_to_fit();
|
||||
}
|
||||
int AutoCorrector::Dictionary::rank(const std::wstring &text) const
|
||||
{
|
||||
auto key = utf8(lower(text));
|
||||
size_t begin = 0, end = offsets.size();
|
||||
while (begin < end)
|
||||
{
|
||||
auto mid = begin + (end - begin) / 2;
|
||||
if (word(mid) < key)
|
||||
begin = mid + 1;
|
||||
else
|
||||
end = mid;
|
||||
}
|
||||
if (begin == offsets.size() || word(begin) != key)
|
||||
return 0;
|
||||
auto start = offsets[begin] + word(begin).size() + 1;
|
||||
int rank{};
|
||||
std::from_chars(bytes.data() + start, bytes.data() + bytes.size(), rank);
|
||||
return rank;
|
||||
}
|
||||
void AutoCorrector::configure(const std::filesystem::path &folder, const std::vector<std::wstring> &wanted)
|
||||
{
|
||||
if (languages == wanted)
|
||||
return;
|
||||
std::map<std::wstring, Dictionary> next;
|
||||
for (auto &lang : wanted)
|
||||
{
|
||||
if (lang != L"en" && lang != L"uk" && lang != L"ru")
|
||||
throw std::runtime_error("Unsupported dictionary language");
|
||||
next.emplace(lang, Dictionary(folder / (lang + L".txt")));
|
||||
}
|
||||
dictionaries = std::move(next);
|
||||
languages = wanted;
|
||||
}
|
||||
void AutoCorrector::reject(std::wstring word, const Settings &settings)
|
||||
{
|
||||
while (!word.empty() && openingDelimiter(word.front(), settings))
|
||||
word.erase(word.begin());
|
||||
while (!word.empty() && trailingDelimiter(word.back(), settings))
|
||||
word.pop_back();
|
||||
if (rejected.size() >= 128)
|
||||
rejected.clear();
|
||||
rejected.insert(lower(word));
|
||||
}
|
||||
size_t AutoCorrector::memoryBytes() const
|
||||
{
|
||||
size_t bytes = 0;
|
||||
for (auto &[language, d] : dictionaries)
|
||||
bytes += d.bytes.capacity() + d.offsets.capacity() * sizeof(uint32_t);
|
||||
return bytes;
|
||||
}
|
||||
std::optional<AutoSuggestion> AutoCorrector::suggest(const std::wstring &typed, int source,
|
||||
const std::vector<Layout> &layouts,
|
||||
const Settings &settings) const
|
||||
{
|
||||
if (source < 0 || source >= (int)layouts.size() || !layouts[source].convertible)
|
||||
return {};
|
||||
std::wstring word = typed, suffix, prefix;
|
||||
while (!word.empty() && openingDelimiter(word.front(), settings) && prefix.size() < 3)
|
||||
{
|
||||
prefix += word.front();
|
||||
word.erase(word.begin());
|
||||
}
|
||||
while (!word.empty() && trailingDelimiter(word.back(), settings))
|
||||
{
|
||||
suffix.insert(suffix.begin(), word.back());
|
||||
word.pop_back();
|
||||
}
|
||||
if (!safeAutoWord(word, settings.autoMinLength) || suffix.size() > 6 || rejected.contains(lower(word)))
|
||||
return {};
|
||||
for (auto &never : settings.autoNever)
|
||||
if (lower(never) == lower(word))
|
||||
return {};
|
||||
auto sourceLanguage = layoutLanguage(layouts[source]);
|
||||
if (!dictionaries.contains(sourceLanguage))
|
||||
return {};
|
||||
// Valid words from either selected language are never changed.
|
||||
for (auto &[language, d] : dictionaries)
|
||||
if (d.rank(word))
|
||||
return {};
|
||||
std::optional<AutoSuggestion> candidate;
|
||||
int matches = 0;
|
||||
for (int i = 0; i < (int)layouts.size(); ++i)
|
||||
{
|
||||
if (i == source || !layouts[i].convertible ||
|
||||
std::find(settings.cycle.begin(), settings.cycle.end(), layouts[i].id) == settings.cycle.end())
|
||||
continue;
|
||||
auto dictionary = dictionaries.find(layoutLanguage(layouts[i]));
|
||||
if (dictionary == dictionaries.end())
|
||||
continue;
|
||||
auto converted = convert(word, layouts[source], layouts[i], true);
|
||||
if (converted == word || !safeAutoWord(converted, settings.autoMinLength))
|
||||
continue;
|
||||
int rank = dictionary->second.rank(converted);
|
||||
if (!rank)
|
||||
continue;
|
||||
if (++matches > 1)
|
||||
return {};
|
||||
if (settings.autoMaxRank == 0 || rank <= settings.autoMaxRank)
|
||||
candidate = AutoSuggestion{source, i, prefix + converted + suffix};
|
||||
}
|
||||
return candidate;
|
||||
}
|
||||
int matchAutoInput(const Run &run, const std::wstring &text, const std::vector<Layout> &layouts,
|
||||
const Layout &physical, const Settings &settings)
|
||||
{
|
||||
int full = run.match(text, layouts, physical);
|
||||
if (full >= 0 || run.overflow || text.empty())
|
||||
return full;
|
||||
// Ctrl+Shift+Left may omit opening delimiters. Only these delimiters may
|
||||
// be omitted; never accept an arbitrary matching suffix of a longer word.
|
||||
for (int i = 0; i < (int)layouts.size(); ++i)
|
||||
{
|
||||
if (!layouts[i].convertible)
|
||||
continue;
|
||||
std::wstring expected;
|
||||
for (wchar_t c : run.keys)
|
||||
expected += c == L' ' ? L' ' : layouts[i].at(physical.code(c));
|
||||
for (int removed = 0;
|
||||
removed < 3 && !expected.empty() && openingDelimiter(expected.front(), settings); ++removed)
|
||||
{
|
||||
expected.erase(expected.begin());
|
||||
if (lower(expected) == lower(text))
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
bool autoCorrectionAllowed(HWND target, const Settings &settings)
|
||||
{
|
||||
if (!target || GetForegroundWindow() != target)
|
||||
return false;
|
||||
auto process = lower(processName(target));
|
||||
for (auto &excluded : settings.autoExcluded)
|
||||
{
|
||||
auto name = lower(excluded);
|
||||
if (name.ends_with(L".exe"))
|
||||
name.resize(name.size() - 4);
|
||||
if (name == process)
|
||||
return false;
|
||||
}
|
||||
// Fail closed: do not copy from password fields or inaccessible controls.
|
||||
IUIAutomation2 *automation{};
|
||||
if (FAILED(CoCreateInstance(CLSID_CUIAutomation8, nullptr, CLSCTX_INPROC_SERVER, IID_IUIAutomation2,
|
||||
(void **)&automation)))
|
||||
return false;
|
||||
automation->put_ConnectionTimeout(200);
|
||||
automation->put_TransactionTimeout(200);
|
||||
IUIAutomationElement *focused{};
|
||||
HRESULT hr = automation->GetFocusedElement(&focused);
|
||||
automation->Release();
|
||||
if (FAILED(hr) || !focused)
|
||||
return false;
|
||||
BOOL password = TRUE, enabled = FALSE, hasFocus = FALSE;
|
||||
CONTROLTYPEID type{};
|
||||
bool allowed = SUCCEEDED(focused->get_CurrentIsPassword(&password)) && !password &&
|
||||
SUCCEEDED(focused->get_CurrentIsEnabled(&enabled)) && enabled &&
|
||||
SUCCEEDED(focused->get_CurrentHasKeyboardFocus(&hasFocus)) && hasFocus &&
|
||||
SUCCEEDED(focused->get_CurrentControlType(&type)) &&
|
||||
(type == UIA_EditControlTypeId || type == UIA_DocumentControlTypeId);
|
||||
focused->Release();
|
||||
return allowed && GetForegroundWindow() == target;
|
||||
}
|
||||
} // namespace ks
|
||||
@@ -0,0 +1,40 @@
|
||||
#pragma once
|
||||
#include "platform.h"
|
||||
#include <map>
|
||||
#include <set>
|
||||
#include <string_view>
|
||||
namespace ks
|
||||
{
|
||||
struct AutoSuggestion
|
||||
{
|
||||
int source{-1}, target{-1};
|
||||
std::wstring text;
|
||||
};
|
||||
std::wstring layoutLanguage(const Layout &);
|
||||
bool safeAutoWord(const std::wstring &, int minimumLength = 3);
|
||||
bool autoClosingSelection(const std::wstring &, const Settings & = {});
|
||||
int matchAutoInput(const Run &, const std::wstring &, const std::vector<Layout> &, const Layout &,
|
||||
const Settings & = {});
|
||||
bool autoCorrectionAllowed(HWND, const Settings &);
|
||||
class AutoCorrector
|
||||
{
|
||||
struct Dictionary
|
||||
{
|
||||
std::string bytes;
|
||||
std::vector<uint32_t> offsets;
|
||||
std::string_view word(size_t) const;
|
||||
explicit Dictionary(const std::filesystem::path &);
|
||||
int rank(const std::wstring &) const;
|
||||
};
|
||||
std::map<std::wstring, Dictionary> dictionaries;
|
||||
std::vector<std::wstring> languages;
|
||||
std::set<std::wstring> rejected;
|
||||
|
||||
public:
|
||||
void configure(const std::filesystem::path &, const std::vector<std::wstring> &);
|
||||
std::optional<AutoSuggestion> suggest(const std::wstring &, int source, const std::vector<Layout> &,
|
||||
const Settings &) const;
|
||||
void reject(std::wstring word, const Settings & = {});
|
||||
size_t memoryBytes() const;
|
||||
};
|
||||
} // namespace ks
|
||||
+10
@@ -46,6 +46,16 @@ struct Settings
|
||||
{
|
||||
bool recent = true, switchLayout = true, history = true, autostart = false, awake = false;
|
||||
bool elevatedAutostart = false;
|
||||
bool autoCorrect = false;
|
||||
int autoMaxRank = 0;
|
||||
int autoMinLength = 3;
|
||||
std::wstring autoPrefixChars = L"([{«“‘\"";
|
||||
std::wstring autoSuffixChars = L")]}»”’\".,!?:;";
|
||||
std::vector<std::wstring> autoLanguages{L"en", L"uk"};
|
||||
std::vector<std::wstring> autoNever;
|
||||
std::vector<std::wstring> autoExcluded{
|
||||
L"Code", L"rider64", L"devenv", L"WindowsTerminal", L"cmd", L"powershell", L"pwsh",
|
||||
L"Far", L"ConEmu64", L"putty", L"KeePass", L"KeePassXC", L"1Password", L"Bitwarden"};
|
||||
int maxItems = 500, maxMiB = 200, maxImageMiB = 20;
|
||||
std::wstring toggle = L"Ctrl+Alt+Shift+Space", casing = L"Ctrl+Alt+Shift+C",
|
||||
translit = L"Ctrl+Alt+Shift+T", undo = L"Ctrl+Alt+Shift+Z", clipboard = L"Ctrl+Shift+V";
|
||||
|
||||
+47
-1
@@ -107,9 +107,55 @@ wchar_t physicalKey(DWORD scan, bool shift)
|
||||
return (shift ? physicalLayout().shifted : physicalLayout().normal)[i];
|
||||
return 0;
|
||||
}
|
||||
static HWND inputWindow(HWND w)
|
||||
{
|
||||
if (!w || !IsWindow(w))
|
||||
return nullptr;
|
||||
GUITHREADINFO info{sizeof(info)};
|
||||
DWORD thread = w == GetForegroundWindow() ? 0 : GetWindowThreadProcessId(w, nullptr);
|
||||
if (GetGUIThreadInfo(thread, &info) && info.hwndFocus &&
|
||||
(info.hwndFocus == w || IsChild(w, info.hwndFocus) || GetAncestor(info.hwndFocus, GA_ROOT) == w))
|
||||
return info.hwndFocus;
|
||||
return w;
|
||||
}
|
||||
HKL windowLayout(HWND w)
|
||||
{
|
||||
return GetKeyboardLayout(GetWindowThreadProcessId(w, nullptr));
|
||||
HWND input = inputWindow(w);
|
||||
return input ? GetKeyboardLayout(GetWindowThreadProcessId(input, nullptr)) : nullptr;
|
||||
}
|
||||
bool switchWindowLayout(HWND target, HKL layout, const std::function<bool()> &valid)
|
||||
{
|
||||
HWND input = inputWindow(target);
|
||||
auto unchanged = [&]
|
||||
{ return input && valid() && GetForegroundWindow() == target && inputWindow(target) == input; };
|
||||
if (!layout || !unchanged())
|
||||
return false;
|
||||
if (windowLayout(target) == layout)
|
||||
return true;
|
||||
// Windows directs layout requests to the focused control, which can belong
|
||||
// to a different UI thread/process than a Teams/WebView top-level window.
|
||||
if (!PostMessageW(input, WM_INPUTLANGCHANGEREQUEST, 0, (LPARAM)layout))
|
||||
return false;
|
||||
for (int i = 0; i < 30; ++i)
|
||||
{
|
||||
if (!unchanged())
|
||||
return false;
|
||||
if (windowLayout(target) == layout)
|
||||
return true;
|
||||
// Some hosts handle this at their top level instead of the focused child.
|
||||
if (i == 14 && input != target)
|
||||
PostMessageW(target, WM_INPUTLANGCHANGEREQUEST, 0, (LPARAM)layout);
|
||||
Sleep(10);
|
||||
}
|
||||
return unchanged() && windowLayout(target) == layout;
|
||||
}
|
||||
void showRequestedWindow(HWND window)
|
||||
{
|
||||
ShowWindow(window, SW_SHOW);
|
||||
// STARTF_USESHOWWINDOW can override the process's first ShowWindow with
|
||||
// SW_HIDE. An explicit user request must still make the window visible.
|
||||
if (!IsWindowVisible(window))
|
||||
ShowWindow(window, SW_SHOW);
|
||||
}
|
||||
bool activateWindow(HWND window)
|
||||
{
|
||||
|
||||
@@ -24,7 +24,9 @@ std::vector<Layout> readLayouts();
|
||||
const Layout &physicalLayout();
|
||||
wchar_t physicalKey(DWORD scan, bool shift);
|
||||
HKL windowLayout(HWND);
|
||||
bool switchWindowLayout(HWND, HKL, const std::function<bool()> &valid);
|
||||
bool activateWindow(HWND);
|
||||
void showRequestedWindow(HWND);
|
||||
bool ownsClipboard(HWND);
|
||||
bool webViewDescendant(DWORD owner, DWORD host,
|
||||
const std::function<std::optional<std::pair<DWORD, std::wstring>>(DWORD)> &read);
|
||||
|
||||
@@ -62,6 +62,29 @@ void validateSettings(const Settings &s)
|
||||
throw std::runtime_error("Keyboard shortcuts must be unique");
|
||||
used.push_back(*key);
|
||||
}
|
||||
if (s.autoMinLength < 1 || s.autoMinLength > 32)
|
||||
throw std::runtime_error("Minimum word length must be between 1 and 32 letters");
|
||||
if (s.autoMaxRank < 0)
|
||||
throw std::runtime_error("Frequency rank limit must be zero or a positive whole number");
|
||||
for (const auto &characters : {s.autoPrefixChars, s.autoSuffixChars})
|
||||
{
|
||||
if (characters.size() > 32)
|
||||
throw std::runtime_error("Use at most 32 punctuation characters in each delimiter field");
|
||||
for (wchar_t c : characters)
|
||||
if (letter(c) || iswalnum(c) || iswspace(c) || c < 32 || (c >= 0xd800 && c <= 0xdfff))
|
||||
throw std::runtime_error(
|
||||
"Word delimiters must be punctuation, without letters, digits or spaces");
|
||||
}
|
||||
if (s.autoCorrect && s.autoLanguages.size() < 2)
|
||||
throw std::runtime_error("Select at least two languages for automatic correction");
|
||||
if (std::set<std::wstring>(s.autoLanguages.begin(), s.autoLanguages.end()).size() !=
|
||||
s.autoLanguages.size())
|
||||
throw std::runtime_error("Dictionary languages must be unique");
|
||||
for (auto &language : s.autoLanguages)
|
||||
if (language != L"en" && language != L"uk" && language != L"ru")
|
||||
throw std::runtime_error("Unsupported auto-correction language");
|
||||
if (s.autoNever.size() > 10000 || s.autoExcluded.size() > 1000)
|
||||
throw std::runtime_error("Too many auto-correction exceptions");
|
||||
if (s.maxItems < 10 || s.maxItems > 10000 || s.maxMiB < 1 || s.maxMiB > 2048 || s.maxImageMiB < 1 ||
|
||||
s.maxImageMiB > 64)
|
||||
throw std::runtime_error("History limits: 10–10000 items, 1–2048 MiB total, 1–64 MiB per image");
|
||||
@@ -100,6 +123,20 @@ Settings loadSettings(const std::filesystem::path &path, const std::vector<Layou
|
||||
s.autostart = j.value("startWithSystem", false);
|
||||
s.elevatedAutostart = j.value("autostartAsAdministrator", false);
|
||||
s.awake = j.value("preventScreenLock", false);
|
||||
s.autoCorrect = j.value("autoCorrectLayout", false);
|
||||
s.autoMaxRank = j.value("autoCorrectMaxRank", 0);
|
||||
s.autoMinLength = j.value("autoCorrectMinLength", 3);
|
||||
s.autoPrefixChars = str(j, "autoCorrectPrefixChars", s.autoPrefixChars);
|
||||
s.autoSuffixChars = str(j, "autoCorrectSuffixChars", s.autoSuffixChars);
|
||||
for (auto pair : {std::pair{"autoCorrectLanguages", &s.autoLanguages},
|
||||
{"autoCorrectNever", &s.autoNever},
|
||||
{"autoCorrectExcludedApps", &s.autoExcluded}})
|
||||
if (j.contains(pair.first))
|
||||
{
|
||||
pair.second->clear();
|
||||
for (auto &item : j.at(pair.first))
|
||||
pair.second->push_back(wide(item.get<std::string>()));
|
||||
}
|
||||
s.maxItems = std::clamp(j.value("maxItems", 500), 10, 10000);
|
||||
s.maxMiB = std::clamp(j.value("maxTotalMegabytes", 200), 1, 2048);
|
||||
s.maxImageMiB = std::clamp(j.value("maxImageMegabytes", 20), 1, 64);
|
||||
@@ -160,6 +197,19 @@ void saveSettings(const std::filesystem::path &path, const Settings &s)
|
||||
j["layouts"] = Json::array();
|
||||
for (auto &id : s.cycle)
|
||||
j["layouts"].push_back(utf8(id));
|
||||
j["autoCorrectLayout"] = s.autoCorrect;
|
||||
j["autoCorrectMaxRank"] = s.autoMaxRank;
|
||||
j["autoCorrectMinLength"] = s.autoMinLength;
|
||||
j["autoCorrectPrefixChars"] = utf8(s.autoPrefixChars);
|
||||
j["autoCorrectSuffixChars"] = utf8(s.autoSuffixChars);
|
||||
for (auto pair : {std::pair{"autoCorrectLanguages", &s.autoLanguages},
|
||||
{"autoCorrectNever", &s.autoNever},
|
||||
{"autoCorrectExcludedApps", &s.autoExcluded}})
|
||||
{
|
||||
j[pair.first] = Json::array();
|
||||
for (auto &item : *pair.second)
|
||||
j[pair.first].push_back(utf8(item));
|
||||
}
|
||||
j["layoutHotkeys"] = Json::array();
|
||||
for (size_t i = 0; i < std::min(s.directIds.size(), s.direct.size()); ++i)
|
||||
j["layoutHotkeys"].push_back({{"layoutId", utf8(s.directIds[i])}, {"hotkey", utf8(s.direct[i])}});
|
||||
|
||||
+170
-5
@@ -8,6 +8,7 @@
|
||||
#include <map>
|
||||
#include <richedit.h>
|
||||
#include <set>
|
||||
#include <shellapi.h>
|
||||
#include <sstream>
|
||||
|
||||
namespace ks
|
||||
@@ -648,6 +649,7 @@ void fillSettings(HWND w, SettingsView *v)
|
||||
check(w, 204, s.autostart);
|
||||
check(w, 206, s.elevatedAutostart);
|
||||
check(w, 205, s.awake);
|
||||
check(w, 207, s.autoCorrect);
|
||||
value(w, 210, s.toggle);
|
||||
value(w, 211, s.casing);
|
||||
value(w, 212, s.translit);
|
||||
@@ -695,6 +697,7 @@ Settings collectSettings(HWND w, SettingsView *v)
|
||||
s.autostart = checked(w, 204);
|
||||
s.elevatedAutostart = checked(w, 206);
|
||||
s.awake = checked(w, 205);
|
||||
s.autoCorrect = checked(w, 207);
|
||||
s.toggle = value(w, 210);
|
||||
s.casing = value(w, 211);
|
||||
s.translit = value(w, 212);
|
||||
@@ -723,6 +726,158 @@ Settings collectSettings(HWND w, SettingsView *v)
|
||||
validateSettings(s);
|
||||
return s;
|
||||
}
|
||||
struct AutoOptionsView
|
||||
{
|
||||
HWND parent;
|
||||
SettingsView *settings;
|
||||
};
|
||||
std::wstring commaList(const std::vector<std::wstring> &items)
|
||||
{
|
||||
std::wstring text;
|
||||
for (auto &item : items)
|
||||
{
|
||||
if (!text.empty())
|
||||
text += L", ";
|
||||
text += item;
|
||||
}
|
||||
return text;
|
||||
}
|
||||
std::vector<std::wstring> parseCommaList(const std::wstring &text)
|
||||
{
|
||||
std::vector<std::wstring> items;
|
||||
std::wistringstream input(text);
|
||||
std::wstring item;
|
||||
while (std::getline(input, item, L','))
|
||||
{
|
||||
auto first = item.find_first_not_of(L" \t\r\n"), last = item.find_last_not_of(L" \t\r\n");
|
||||
if (first != item.npos)
|
||||
items.push_back(item.substr(first, last - first + 1));
|
||||
}
|
||||
return items;
|
||||
}
|
||||
LRESULT CALLBACK autoOptionsProc(HWND w, UINT msg, WPARAM wp, LPARAM lp)
|
||||
{
|
||||
auto *view = (AutoOptionsView *)GetWindowLongPtrW(w, GWLP_USERDATA);
|
||||
if (!view)
|
||||
return DefWindowProcW(w, msg, wp, lp);
|
||||
if (msg == WM_COMMAND && LOWORD(wp) == 407)
|
||||
{
|
||||
auto folder = std::filesystem::path(executable()).parent_path() / L"dictionaries";
|
||||
if ((INT_PTR)ShellExecuteW(w, L"open", folder.c_str(), nullptr, nullptr, SW_SHOWNORMAL) <= 32)
|
||||
MessageBoxW(w, L"Could not open the dictionary folder.", L"KSwitcher", MB_OK | MB_ICONERROR);
|
||||
return 0;
|
||||
}
|
||||
if (msg == WM_COMMAND && LOWORD(wp) == IDOK)
|
||||
{
|
||||
try
|
||||
{
|
||||
auto draft = view->settings->draft;
|
||||
auto lengthText = value(w, 410);
|
||||
if (lengthText.empty() || lengthText.size() > 2 ||
|
||||
lengthText.find_first_not_of(L"0123456789") != lengthText.npos)
|
||||
throw std::runtime_error("Minimum word length must be between 1 and 32 letters");
|
||||
draft.autoMinLength = std::stoi(lengthText);
|
||||
auto rankText = value(w, 406);
|
||||
if (rankText.empty() || rankText.find_first_not_of(L"0123456789") != rankText.npos)
|
||||
throw std::runtime_error("Enter a non-negative whole number for the frequency rank limit");
|
||||
try
|
||||
{
|
||||
draft.autoMaxRank = std::stoi(rankText);
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
throw std::runtime_error("Frequency rank limit is too large");
|
||||
}
|
||||
draft.autoLanguages.clear();
|
||||
if (checked(w, 401))
|
||||
draft.autoLanguages.push_back(L"en");
|
||||
if (checked(w, 402))
|
||||
draft.autoLanguages.push_back(L"uk");
|
||||
if (checked(w, 403))
|
||||
draft.autoLanguages.push_back(L"ru");
|
||||
draft.autoNever = parseCommaList(value(w, 405));
|
||||
draft.autoExcluded = parseCommaList(value(w, 404));
|
||||
draft.autoPrefixChars = value(w, 408);
|
||||
draft.autoSuffixChars = value(w, 409);
|
||||
if (draft.autoLanguages.size() < 2)
|
||||
throw std::runtime_error("Select at least two languages");
|
||||
validateSettings(draft);
|
||||
view->settings->draft.autoLanguages = draft.autoLanguages;
|
||||
view->settings->draft.autoNever = draft.autoNever;
|
||||
view->settings->draft.autoExcluded = draft.autoExcluded;
|
||||
view->settings->draft.autoMaxRank = draft.autoMaxRank;
|
||||
view->settings->draft.autoMinLength = draft.autoMinLength;
|
||||
view->settings->draft.autoPrefixChars = draft.autoPrefixChars;
|
||||
view->settings->draft.autoSuffixChars = draft.autoSuffixChars;
|
||||
DestroyWindow(w);
|
||||
}
|
||||
catch (const std::exception &e)
|
||||
{
|
||||
error(w, e);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
if ((msg == WM_COMMAND && LOWORD(wp) == IDCANCEL) || msg == WM_CLOSE)
|
||||
{
|
||||
DestroyWindow(w);
|
||||
return 0;
|
||||
}
|
||||
if (msg == WM_DPICHANGED)
|
||||
{
|
||||
resizeForDpi(w, wp, lp);
|
||||
return 0;
|
||||
}
|
||||
if (msg == WM_NCDESTROY)
|
||||
{
|
||||
EnableWindow(view->parent, TRUE);
|
||||
SetForegroundWindow(view->parent);
|
||||
disposeFont(w);
|
||||
delete view;
|
||||
SetWindowLongPtrW(w, GWLP_USERDATA, 0);
|
||||
}
|
||||
return DefWindowProcW(w, msg, wp, lp);
|
||||
}
|
||||
void showAutoOptions(HWND parent, SettingsView *v)
|
||||
{
|
||||
HWND w = makeWindow(*v->app, L"KSwitcher.AutoOptions", L"Automatic layout correction", autoOptionsProc,
|
||||
560, 680);
|
||||
SetWindowLongPtrW(w, GWLP_HWNDPARENT, (LONG_PTR)parent);
|
||||
SetWindowLongPtrW(w, GWLP_USERDATA, (LONG_PTR) new AutoOptionsView{parent, v});
|
||||
label(w, L"Offline dictionaries (only layouts in your cycle are used)", 18, 16, 525);
|
||||
checkbox(w, L"English", 401, 18, 48, 150);
|
||||
checkbox(w, L"Ukrainian", 402, 188, 48, 150);
|
||||
checkbox(w, L"Russian", 403, 358, 48, 150);
|
||||
for (auto &lang : v->draft.autoLanguages)
|
||||
check(w, lang == L"en" ? 401 : lang == L"uk" ? 402 : 403, true);
|
||||
label(w, L"Excluded applications (process names, comma-separated)", 18, 94, 525);
|
||||
edit(w, 404, 18, 122, 524, 84, ES_MULTILINE | WS_VSCROLL | ES_AUTOVSCROLL);
|
||||
value(w, 404, commaList(v->draft.autoExcluded));
|
||||
label(w, L"Personal dictionary: never correct these words (comma-separated)", 18, 220, 525);
|
||||
edit(w, 405, 18, 248, 524, 84, ES_MULTILINE | WS_VSCROLL | ES_AUTOVSCROLL);
|
||||
value(w, 405, commaList(v->draft.autoNever));
|
||||
label(w, L"Corrects after Space and a short pause.", 18, 344, 525);
|
||||
label(w, L"Undo skips this word until KSwitcher restarts.", 18, 366, 525);
|
||||
label(w, L"Frequency rank limit (0 = entire dictionary)", 18, 400, 390);
|
||||
edit(w, 406, 420, 396, 122, 26, ES_NUMBER);
|
||||
value(w, 406, std::to_wstring(v->draft.autoMaxRank));
|
||||
label(w, L"Minimum word length (1–32 letters)", 18, 434, 390);
|
||||
edit(w, 410, 420, 430, 122, 26, ES_NUMBER);
|
||||
value(w, 410, std::to_wstring(v->draft.autoMinLength));
|
||||
label(w, L"Characters before a word", 18, 474, 250);
|
||||
label(w, L"Characters after a word", 290, 474, 250);
|
||||
edit(w, 408, 18, 500, 252);
|
||||
edit(w, 409, 290, 500, 252);
|
||||
value(w, 408, v->draft.autoPrefixChars);
|
||||
value(w, 409, v->draft.autoSuffixChars);
|
||||
label(w, L"Enter punctuation without spaces or separators. These characters are kept.", 18, 530, 525);
|
||||
button(w, L"Open dictionary folder", 407, 18, 562, 220);
|
||||
label(w, L"After editing files, click OK here, then Save in Settings to reload.", 18, 600, 525);
|
||||
button(w, L"OK", IDOK, 326, 632, 100);
|
||||
button(w, L"Cancel", IDCANCEL, 440, 632, 100);
|
||||
EnableWindow(parent, FALSE);
|
||||
showRequestedWindow(w);
|
||||
SetForegroundWindow(w);
|
||||
}
|
||||
LRESULT CALLBACK settingsProc(HWND w, UINT msg, WPARAM wp, LPARAM lp)
|
||||
{
|
||||
if (msg == WM_DPICHANGED)
|
||||
@@ -802,6 +957,11 @@ LRESULT CALLBACK settingsProc(HWND w, UINT msg, WPARAM wp, LPARAM lp)
|
||||
saveSettings(path, s);
|
||||
return 0;
|
||||
}
|
||||
if (id == 255)
|
||||
{
|
||||
showAutoOptions(w, v);
|
||||
return 0;
|
||||
}
|
||||
if (id == 252)
|
||||
{
|
||||
showSnippets(*v->app);
|
||||
@@ -1040,7 +1200,10 @@ void showSettings(App &app)
|
||||
if (app.settingsWindow)
|
||||
{
|
||||
ShowWindow(app.settingsWindow, SW_RESTORE);
|
||||
SetForegroundWindow(app.settingsWindow);
|
||||
HWND target = app.settingsWindow;
|
||||
if (!IsWindowEnabled(target))
|
||||
target = GetLastActivePopup(target);
|
||||
activateWindow(target);
|
||||
return;
|
||||
}
|
||||
HWND w = makeWindow(app, L"KSwitcher.Settings", L"KSwitcher settings", settingsProc, 780, 660);
|
||||
@@ -1060,6 +1223,7 @@ void showSettings(App &app)
|
||||
label(w, names[i], 420, 48 + i * 30, 115);
|
||||
hotkeyEdit(app, w, 210 + i, 540, 44 + i * 30, 220);
|
||||
}
|
||||
checkbox(w, L"Automatically correct layout (experimental)", 207, 420, 195, 350);
|
||||
label(w, L"Layout cycle — top to bottom", 18, 210, 400);
|
||||
control(w, L"LISTBOX", L"", WS_TABSTOP | WS_VSCROLL | LBS_NOTIFY | LBS_NOINTEGRALHEIGHT | WS_BORDER, 220,
|
||||
18, 232, 470, 190);
|
||||
@@ -1080,14 +1244,15 @@ void showSettings(App &app)
|
||||
button(w, L"Clear history…", 254, 580, 474, 180);
|
||||
label(w, L"Exclude applications (process names, comma-separated)", 18, 516, 700);
|
||||
edit(w, 243, 18, 544, 742);
|
||||
button(w, L"Auto-correction options…", 255, 18, 575, 240);
|
||||
button(w, L"Import settings…", 250, 18, 606, 145);
|
||||
button(w, L"Export settings…", 251, 173, 606, 145);
|
||||
button(w, L"Snippets…", 252, 328, 606, 110);
|
||||
button(w, L"Save", IDOK, 548, 606, 100);
|
||||
button(w, L"Cancel", IDCANCEL, 660, 606, 100);
|
||||
fillSettings(w, (SettingsView *)GetWindowLongPtrW(w, GWLP_USERDATA));
|
||||
ShowWindow(w, SW_SHOW);
|
||||
SetForegroundWindow(w);
|
||||
showRequestedWindow(w);
|
||||
activateWindow(w);
|
||||
}
|
||||
void showSnippets(App &app)
|
||||
{
|
||||
@@ -1112,7 +1277,7 @@ void showSnippets(App &app)
|
||||
button(w, L"Save", IDOK, 510, 478, 100);
|
||||
button(w, L"Cancel", IDCANCEL, 624, 478, 100);
|
||||
fillSnippets(w, (SnippetView *)GetWindowLongPtrW(w, GWLP_USERDATA));
|
||||
ShowWindow(w, SW_SHOW);
|
||||
showRequestedWindow(w);
|
||||
SetForegroundWindow(w);
|
||||
}
|
||||
void showShortcuts(App &app)
|
||||
@@ -1145,7 +1310,7 @@ void showEntry(App &app, const std::wstring &id)
|
||||
WS_THICKFRAME | WS_MAXIMIZEBOX);
|
||||
auto field = edit(w, 1, 10, 10, 700, 480, ES_MULTILINE | ES_READONLY | ES_AUTOVSCROLL | WS_VSCROLL);
|
||||
SetWindowTextW(field, text.c_str());
|
||||
ShowWindow(w, SW_SHOW);
|
||||
showRequestedWindow(w);
|
||||
}
|
||||
catch (const std::exception &e)
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user