Release v0.6.28: typo correction, partial sync and selection diagnostics
This commit is contained in:
+75
-7
@@ -73,6 +73,8 @@ class Transaction
|
||||
Clip snapshot;
|
||||
DWORD expected{};
|
||||
bool captured{};
|
||||
mutable bool invalidLogged{};
|
||||
unsigned copyNumber{};
|
||||
std::wstring adopted;
|
||||
|
||||
public:
|
||||
@@ -108,8 +110,18 @@ class Transaction
|
||||
}
|
||||
bool valid() const
|
||||
{
|
||||
return target && IsWindow(target) && GetForegroundWindow() == target &&
|
||||
app.inputVersion.load() == version;
|
||||
auto foreground = GetForegroundWindow();
|
||||
auto currentVersion = app.inputVersion.load();
|
||||
bool ok = target && IsWindow(target) && foreground == target && currentVersion == version;
|
||||
if (!ok && !invalidLogged)
|
||||
{
|
||||
invalidLogged = true;
|
||||
diagnosticEvent(L"Context invalid: target=" + std::to_wstring((uintptr_t)target) +
|
||||
L" alive=" + std::to_wstring(IsWindow(target)) + L" foreground=" +
|
||||
std::to_wstring((uintptr_t)foreground) + L" expectedVersion=" + std::to_wstring(version) +
|
||||
L" currentVersion=" + std::to_wstring(currentVersion));
|
||||
}
|
||||
return ok;
|
||||
}
|
||||
void check()
|
||||
{
|
||||
@@ -144,10 +156,14 @@ class Transaction
|
||||
}
|
||||
std::wstring copy()
|
||||
{
|
||||
const auto started = GetTickCount64();
|
||||
const auto number = ++copyNumber;
|
||||
if (!valid())
|
||||
return {};
|
||||
write(textClip(L"\x2063KSwitcher-" + std::to_wstring(GetTickCount64())));
|
||||
DWORD before = expected;
|
||||
diagnosticEvent(L"Copy begin number=" + std::to_wstring(number) + L" sentinelSequence=" +
|
||||
std::to_wstring(before) + L" target=" + std::to_wstring((uintptr_t)target));
|
||||
if (!valid() || !chord(L'C', MOD_CONTROL))
|
||||
{
|
||||
diagnosticEvent(L"Copy failed: context changed or simulated Ctrl+C blocked");
|
||||
@@ -166,6 +182,10 @@ class Transaction
|
||||
DWORD now = GetClipboardSequenceNumber();
|
||||
if (now != changed)
|
||||
{
|
||||
diagnosticEvent(L"Copy sequence number=" + std::to_wstring(number) + L" from=" +
|
||||
std::to_wstring(changed) + L" to=" + std::to_wstring(now) + L" elapsedMs=" +
|
||||
std::to_wstring(GetTickCount64() - started) + L" owner=" +
|
||||
std::to_wstring((uintptr_t)GetClipboardOwner()));
|
||||
changedAgain |= changed != before;
|
||||
changed = now;
|
||||
stable = GetTickCount64();
|
||||
@@ -183,7 +203,8 @@ class Transaction
|
||||
return {};
|
||||
if (!chord(L'C', MOD_CONTROL))
|
||||
return {};
|
||||
diagnosticEvent(L"Copy retry: clipboard unchanged after Ctrl+C");
|
||||
diagnosticEvent(L"Copy retry: clipboard unchanged after Ctrl+C number=" + std::to_wstring(number) +
|
||||
L" attempt=" + std::to_wstring(attempts + 1) + L" elapsedMs=" + std::to_wstring(GetTickCount64() - started));
|
||||
++attempts;
|
||||
retryAt = GetTickCount64() + 250;
|
||||
}
|
||||
@@ -191,6 +212,10 @@ class Transaction
|
||||
}
|
||||
if (changed == before || !valid())
|
||||
{
|
||||
diagnosticEvent(L"Copy end number=" + std::to_wstring(number) + L" attempts=" +
|
||||
std::to_wstring(attempts) + L" elapsedMs=" + std::to_wstring(GetTickCount64() - started) +
|
||||
L" sequence=" + std::to_wstring(GetClipboardSequenceNumber()) + L" clipboardOpenBy=" +
|
||||
std::to_wstring((uintptr_t)GetOpenClipboardWindow()));
|
||||
diagnosticEvent(changed == before ? L"Copy failed: clipboard timeout (2000 ms)" :
|
||||
L"Copy failed: context changed");
|
||||
return {};
|
||||
@@ -201,6 +226,9 @@ class Transaction
|
||||
expected = changed;
|
||||
auto clip = readClipboard(app.window);
|
||||
auto text = clip.text();
|
||||
diagnosticEvent(L"Copy result number=" + std::to_wstring(number) + L" attempts=" +
|
||||
std::to_wstring(attempts) + L" elapsedMs=" + std::to_wstring(GetTickCount64() - started) +
|
||||
L" text=" + diagnosticText(text));
|
||||
adopted = text;
|
||||
check();
|
||||
for (auto &f : clip.formats)
|
||||
@@ -233,6 +261,27 @@ class Transaction
|
||||
const std::function<bool(const std::wstring &)> &suffixMatches = {})
|
||||
{
|
||||
// WebView word boundaries are only candidates; exact verification is mandatory.
|
||||
diagnosticEvent(L"Selection begin steps=" + std::to_wstring(count) + L" expected=" + diagnosticText(wordCandidate));
|
||||
GUITHREADINFO gui{sizeof(gui)};
|
||||
if (GetGUIThreadInfo(GetWindowThreadProcessId(target, nullptr), &gui))
|
||||
{
|
||||
wchar_t cls[128]{};
|
||||
GetClassNameW(gui.hwndFocus, cls, 128);
|
||||
std::wstring offsets = L"unavailable";
|
||||
if (_wcsicmp(cls, L"Edit") == 0)
|
||||
{
|
||||
DWORD_PTR range{};
|
||||
if (SendMessageTimeoutW(gui.hwndFocus, EM_GETSEL, 0, 0,
|
||||
SMTO_ABORTIFHUNG | SMTO_BLOCK, 25, &range) && (DWORD)range != 0xffffffff)
|
||||
offsets = std::to_wstring(LOWORD(range)) + L"," + std::to_wstring(HIWORD(range));
|
||||
}
|
||||
diagnosticEvent(L"Selection editor class=" + std::wstring(cls) + L" focus=" +
|
||||
std::to_wstring((uintptr_t)gui.hwndFocus) + L" caretWindow=" + std::to_wstring((uintptr_t)gui.hwndCaret) +
|
||||
L" caretRect=" + std::to_wstring(gui.rcCaret.left) + L"," + std::to_wstring(gui.rcCaret.top) +
|
||||
L"," + std::to_wstring(gui.rcCaret.right) + L"," + std::to_wstring(gui.rcCaret.bottom) +
|
||||
L" layout=" + std::to_wstring((uintptr_t)windowLayout(target)) + L" selectionOffsets=" + offsets);
|
||||
}
|
||||
else diagnosticEvent(L"Selection editor: focus/caret/selection unavailable");
|
||||
if (needsSelectionDelay(target) && !wordCandidate.empty() &&
|
||||
std::all_of(wordCandidate.begin(), wordCandidate.end(),
|
||||
[](wchar_t c) { return letter(c) || iswdigit(c); }))
|
||||
@@ -241,6 +290,7 @@ class Transaction
|
||||
return {};
|
||||
Sleep(25);
|
||||
auto text = copy();
|
||||
diagnosticEvent(L"Selection word-boundary match=" + std::to_wstring(matches(text)) + L" copied=" + diagnosticText(text));
|
||||
if (valid() && matches(text))
|
||||
{
|
||||
diagnosticEvent(L"Selection: verified word boundary");
|
||||
@@ -251,6 +301,7 @@ class Transaction
|
||||
Sleep(25);
|
||||
}
|
||||
auto text = select(count) ? copy() : std::wstring{};
|
||||
diagnosticEvent(L"Selection initial match=" + std::to_wstring(matches(text)) + L" copied=" + diagnosticText(text));
|
||||
// Chromium editors can apply only part of a burst of Shift+Left events.
|
||||
// Extend only a verified suffix; never replace a partial or unrelated range.
|
||||
if (valid() && !matches(text) && !text.empty() &&
|
||||
@@ -263,10 +314,11 @@ class Transaction
|
||||
{
|
||||
if (!valid() || !chord(VK_LEFT, MOD_SHIFT))
|
||||
return {};
|
||||
diagnosticEvent(L"Selection recovery Shift+Left sent=" + std::to_wstring(i + 1) + L" total=" + std::to_wstring(missing));
|
||||
Sleep(25);
|
||||
}
|
||||
text = copy();
|
||||
diagnosticEvent(L"Selection: suffix recovery match=" + std::to_wstring(matches(text)));
|
||||
diagnosticEvent(L"Selection: suffix recovery match=" + std::to_wstring(matches(text)) + L" copied=" + diagnosticText(text));
|
||||
}
|
||||
return text;
|
||||
}
|
||||
@@ -532,6 +584,7 @@ void App::applySettings(const Settings &requested)
|
||||
run.reset();
|
||||
last.reset();
|
||||
++inputVersion;
|
||||
if (busy) diagnosticEvent(L"Input context changed: settings applied version=" + std::to_wstring(inputVersion.load()));
|
||||
}
|
||||
// Clear on the worker so the next check reloads edits to dictionary files.
|
||||
enqueue([this] { autoCorrector.configure({}, {}); });
|
||||
@@ -560,6 +613,7 @@ void App::reloadLayouts()
|
||||
run.reset();
|
||||
last.reset();
|
||||
++inputVersion;
|
||||
if (busy) diagnosticEvent(L"Input context changed: layouts reloaded version=" + std::to_wstring(inputVersion.load()));
|
||||
}
|
||||
registerHotkeys(settings);
|
||||
}
|
||||
@@ -589,6 +643,7 @@ void App::resetInput()
|
||||
{
|
||||
std::lock_guard lock(stateMutex);
|
||||
++inputVersion;
|
||||
if (busy) diagnosticEvent(L"Input context changed: reset version=" + std::to_wstring(inputVersion.load()));
|
||||
run.reset();
|
||||
last.reset();
|
||||
}
|
||||
@@ -606,6 +661,9 @@ void App::keyboard(const KBDLLHOOKSTRUCT &k, UINT mods)
|
||||
if (isHotkey(vk, mods))
|
||||
return;
|
||||
++inputVersion;
|
||||
if (busy) diagnosticEvent(L"Input context changed: keyboard vk=" + std::to_wstring(vk) +
|
||||
L" modifiers=" + std::to_wstring(mods) + L" injected=" + std::to_wstring((k.flags & LLKHF_INJECTED) != 0) +
|
||||
L" version=" + std::to_wstring(inputVersion.load()));
|
||||
last.reset();
|
||||
HWND fg = GetForegroundWindow();
|
||||
HKL layout = windowLayout(fg);
|
||||
@@ -839,7 +897,10 @@ LRESULT CALLBACK App::mouseProc(int code, WPARAM wp, LPARAM lp)
|
||||
// KSwitcher generates no mouse input. Remote clicks invalidate a typing
|
||||
// range just like local clicks, including while a transaction is pending.
|
||||
if (wp == WM_LBUTTONDOWN || wp == WM_RBUTTONDOWN || wp == WM_MBUTTONDOWN || wp == WM_MOUSEWHEEL)
|
||||
{
|
||||
if (current->busy) diagnosticEvent(L"Input context changed: mouse message=" + std::to_wstring(wp));
|
||||
current->resetInput();
|
||||
}
|
||||
}
|
||||
return CallNextHookEx(nullptr, code, wp, lp);
|
||||
}
|
||||
@@ -1010,9 +1071,10 @@ void App::transform(int command, int snippet)
|
||||
!(command == 0 && reason == L"paste-sent-unverified")},
|
||||
{"elapsedMs", GetTickCount64() - start}, {"foregroundSame", GetForegroundWindow() == target},
|
||||
{"inputSame", app.inputVersion.load() == version}, {"original", bounded(text)},
|
||||
{"expected", bounded(result)}, {"actual", bounded(actual)},
|
||||
{"originalUnits", text.size()}, {"expectedUnits", result.size()}, {"actualUnits", actual.size()},
|
||||
{"textTruncated", text.size() > 4096 || result.size() > 4096 || actual.size() > 4096}};
|
||||
{"expected", bounded(verification == L"selection-mismatch" ? expected : result)}, {"actual", bounded(actual)},
|
||||
{"selectionExpected", bounded(expected)},
|
||||
{"originalUnits", text.size()}, {"expectedUnits", (verification == L"selection-mismatch" ? expected : result).size()}, {"actualUnits", actual.size()},
|
||||
{"textTruncated", text.size() > 4096 || result.size() > 4096 || actual.size() > 4096 || expected.size() > 4096}};
|
||||
diagnosticOperation(record.dump(-1, ' ', true, DiagnosticJson::error_handler_t::replace));
|
||||
}
|
||||
catch (...) {} // Logging must never abort a completed input transaction.
|
||||
@@ -1135,6 +1197,8 @@ void App::transform(int command, int snippet)
|
||||
if (newlines(text) != newlines(previous->result))
|
||||
{
|
||||
reason = L"undo-verification-mismatch";
|
||||
actual = text;
|
||||
verification = L"selection-mismatch";
|
||||
if (tx.valid())
|
||||
chord(VK_RIGHT);
|
||||
return;
|
||||
@@ -1177,6 +1241,8 @@ void App::transform(int command, int snippet)
|
||||
std::to_wstring(recent.keys.size()) + L" match=" + std::to_wstring(hint));
|
||||
if (hint < 0)
|
||||
{
|
||||
actual = text;
|
||||
verification = L"selection-mismatch";
|
||||
reason = text.empty() ? L"recent-copy-empty" : L"recent-verification-mismatch";
|
||||
if (tx.valid())
|
||||
chord(VK_RIGHT);
|
||||
@@ -1194,6 +1260,8 @@ void App::transform(int command, int snippet)
|
||||
{ return newlines(candidate) == newlines(previous->result); });
|
||||
if (newlines(text) != newlines(previous->result))
|
||||
{
|
||||
actual = text;
|
||||
verification = L"selection-mismatch";
|
||||
reason = L"previous-result-verification-mismatch";
|
||||
if (tx.valid())
|
||||
chord(VK_RIGHT);
|
||||
|
||||
+5
-2
@@ -35,7 +35,10 @@ std::wstring App::syncCommand(int action, const std::vector<SyncChoice> &choices
|
||||
auto base = action == 1 || action == 4 ? loadSyncBase(data, c) : Bytes{};
|
||||
result = synchronize(*provider, base, sharedSyncSettings(snapshot),
|
||||
action == 2 ? SyncAction::Download : action == 3 ? SyncAction::Upload : SyncAction::Automatic,
|
||||
[&](const Bytes &b) { restoreSyncSettings(b, snapshot, availableLayouts); }, choices);
|
||||
[&](const Bytes &b) { restoreSyncSettings(b, snapshot, availableLayouts, c.keepLocalLayouts); }, choices,
|
||||
c.keepLocalLayouts);
|
||||
if (result.success && c.keepLocalLayouts)
|
||||
result.status += L" Partial sync: this PC's layouts and their shortcuts were kept; server layouts were preserved.";
|
||||
}
|
||||
catch (const nlohmann::json::exception &) { result.status = L"Invalid sync data or server response."; }
|
||||
catch (const std::exception &e) { result.status = wide(e.what()); }
|
||||
@@ -65,7 +68,7 @@ void App::pollSync()
|
||||
nextSync = GetTickCount64() + 2000;
|
||||
return;
|
||||
}
|
||||
auto next = restoreSyncSettings(syncResult.settings, settings, layouts);
|
||||
auto next = restoreSyncSettings(syncResult.settings, settings, layouts, syncConnection.keepLocalLayouts);
|
||||
if (encodeSettings(next) != syncSnapshot)
|
||||
{
|
||||
atomicWrite(data / L"settings-before-sync.bin", protect(syncSnapshot));
|
||||
|
||||
+66
-1
@@ -164,6 +164,24 @@ std::optional<AutoSuggestion> AutoCorrector::suggest(const std::wstring &typed,
|
||||
auto sourceLanguage = layoutLanguage(layouts[source]);
|
||||
if (!dictionaries.contains(sourceLanguage))
|
||||
return {};
|
||||
const std::wstring alphabet = sourceLanguage == L"en" ? L"abcdefghijklmnopqrstuvwxyz" :
|
||||
sourceLanguage == L"uk" ? L"абвгґдеєжзиіїйклмнопрстуфхцчшщьюя" : L"абвгдеёжзийклмнопрстуфхцчшщъыьэюя";
|
||||
auto small = lower(word);
|
||||
bool sameLanguage = safeAutoWord(word, settings.autoMinLength) &&
|
||||
small.find_first_not_of(alphabet) == small.npos &&
|
||||
std::find(settings.cycle.begin(), settings.cycle.end(), layouts[source].id) != settings.cycle.end();
|
||||
auto sameLayout = [&](std::wstring text)
|
||||
{
|
||||
if (word != small) text[0] = upper(text.substr(0, 1))[0];
|
||||
return AutoSuggestion{source, source, prefix + text + suffix};
|
||||
};
|
||||
if (settings.autoReplace && sameLanguage)
|
||||
{
|
||||
auto replacement = settings.autoReplacements.find(small);
|
||||
if (replacement != settings.autoReplacements.end() &&
|
||||
replacement->second.find_first_not_of(alphabet) == small.npos)
|
||||
return sameLayout(replacement->second);
|
||||
}
|
||||
auto personal = [&](const std::wstring &language, const std::wstring &candidate)
|
||||
{
|
||||
auto found = settings.autoWords.find(language);
|
||||
@@ -203,7 +221,54 @@ std::optional<AutoSuggestion> AutoCorrector::suggest(const std::wstring &typed,
|
||||
(!candidate || priority(i) < priority(candidate->target)))
|
||||
candidate = AutoSuggestion{source, i, prefix + converted + suffix};
|
||||
}
|
||||
return candidate;
|
||||
if (candidate || matchedText || !settings.autoTypos || !sameLanguage || word.size() < 4)
|
||||
return candidate;
|
||||
// Generate only single edits. Stop at a second distinct dictionary hit,
|
||||
// including rare hits: frequency limits must never hide ambiguity.
|
||||
std::wstring found;
|
||||
int foundRank = 0;
|
||||
std::set<std::wstring> personalTargets;
|
||||
if (auto it = settings.autoWords.find(sourceLanguage); it != settings.autoWords.end())
|
||||
for (const auto &text : it->second) personalTargets.insert(lower(text));
|
||||
auto consider = [&](const std::wstring &text)
|
||||
{
|
||||
if (text == small || text == found || text.size() < 4 || text.size() > 32) return true;
|
||||
int rank = personalTargets.contains(text) ? 1 : dictionaries.at(sourceLanguage).rank(text);
|
||||
if (!rank) return true;
|
||||
if (!found.empty()) return false;
|
||||
found = text;
|
||||
foundRank = rank;
|
||||
return true;
|
||||
};
|
||||
for (size_t i = 0; i < small.size(); ++i)
|
||||
{
|
||||
auto text = small;
|
||||
text.erase(i, 1);
|
||||
if (!consider(text)) return {};
|
||||
if (i + 1 < small.size())
|
||||
{
|
||||
text = small;
|
||||
std::swap(text[i], text[i + 1]);
|
||||
if (!consider(text)) return {};
|
||||
}
|
||||
for (wchar_t c : alphabet)
|
||||
{
|
||||
text = small;
|
||||
text[i] = c;
|
||||
if (!consider(text)) return {};
|
||||
}
|
||||
}
|
||||
if (small.size() < 32)
|
||||
for (size_t i = 0; i <= small.size(); ++i)
|
||||
for (wchar_t c : alphabet)
|
||||
{
|
||||
auto text = small;
|
||||
text.insert(i, 1, c);
|
||||
if (!consider(text)) return {};
|
||||
}
|
||||
if (!found.empty() && (settings.autoMaxRank == 0 || foundRank <= settings.autoMaxRank))
|
||||
return sameLayout(found);
|
||||
return {};
|
||||
}
|
||||
int matchAutoInput(const Run &run, const std::wstring &text, const std::vector<Layout> &layouts,
|
||||
const Layout &physical, const Settings &settings)
|
||||
|
||||
@@ -54,6 +54,8 @@ struct Settings
|
||||
bool recent = true, switchLayout = true, history = true, autostart = false, awake = false;
|
||||
bool elevatedAutostart = false;
|
||||
bool autoCorrect = false;
|
||||
bool autoReplace = false, autoTypos = false;
|
||||
std::map<std::wstring, std::wstring> autoReplacements;
|
||||
bool writeLog = true;
|
||||
bool screenshotAutoOpenEditor = false;
|
||||
bool useTesseract = false;
|
||||
|
||||
+2
-1
@@ -37,7 +37,8 @@ inline std::vector<OperationLogRow> parseOperationLog(const std::wstring &conten
|
||||
L"Operation: " + field("attempt") + L"\r\nResult: " + row.outcome +
|
||||
L"\r\nVerification: " + field("verification") + L"\r\nElapsed: " +
|
||||
std::to_wstring(j.value("elapsedMs", 0ULL)) + L" ms\r\n\r\nOriginal:\r\n" +
|
||||
field("original") + L"\r\n\r\nExpected result:\r\n" + field("expected") +
|
||||
field("original") + L"\r\n\r\nExpected selection:\r\n" + field("selectionExpected") +
|
||||
L"\r\n\r\nExpected result / verification target:\r\n" + field("expected") +
|
||||
L"\r\n\r\nRead back:\r\n" + field("actual") + L"\r\n\r\nForeground unchanged: " +
|
||||
(j.value("foregroundSame", false) ? L"yes" : L"no") + L"\r\nInput context unchanged: " +
|
||||
(j.value("inputSame", false) ? L"yes" : L"no") + L"\r\nText truncated: " +
|
||||
|
||||
+9
-2
@@ -407,12 +407,16 @@ bool selectPrevious(size_t count, const std::function<bool()> &valid)
|
||||
// Shift+Left events can all use the same caret position and select only one
|
||||
// character. Give each change time to settle before sending the next one.
|
||||
const auto target = GetForegroundWindow();
|
||||
diagnosticEvent(L"SelectPrevious requested=" + std::to_wstring(count) +
|
||||
L" mode=" + (needsSelectionDelay(target) ? std::wstring(L"paced-25ms") : std::wstring(L"batch")));
|
||||
if (needsSelectionDelay(target))
|
||||
{
|
||||
for (size_t i = 0; i < count; ++i)
|
||||
{
|
||||
if (!valid() || GetForegroundWindow() != target || !chord(VK_LEFT, MOD_SHIFT))
|
||||
return false;
|
||||
if (i < 32 || i + 1 == count)
|
||||
diagnosticEvent(L"SelectPrevious Shift+Left sent=" + std::to_wstring(i + 1));
|
||||
Sleep(25);
|
||||
}
|
||||
return valid() && GetForegroundWindow() == target;
|
||||
@@ -434,7 +438,10 @@ bool selectPrevious(size_t count, const std::function<bool()> &valid)
|
||||
add(VK_LEFT, KEYEVENTF_EXTENDEDKEY | KEYEVENTF_KEYUP);
|
||||
}
|
||||
add(VK_SHIFT, KEYEVENTF_KEYUP);
|
||||
if (SendInput((UINT)inputs.size(), inputs.data(), sizeof(INPUT)) != inputs.size())
|
||||
auto sent = SendInput((UINT)inputs.size(), inputs.data(), sizeof(INPUT));
|
||||
diagnosticEvent(L"SelectPrevious batch offset=" + std::to_wstring(offset) + L" eventsRequested=" +
|
||||
std::to_wstring(inputs.size()) + L" eventsSent=" + std::to_wstring(sent));
|
||||
if (sent != inputs.size())
|
||||
return false;
|
||||
}
|
||||
return valid();
|
||||
@@ -574,7 +581,7 @@ DiagnosticBuffer diagnosticBuffer;
|
||||
}
|
||||
std::wstring DiagnosticBuffer::record(const std::wstring &message, bool problem, ULONGLONG now)
|
||||
{
|
||||
while (!entries.empty() && (entries.size() >= 40 || now - entries.front().first > 30000))
|
||||
while (!entries.empty() && (entries.size() >= 128 || now - entries.front().first > 30000))
|
||||
entries.pop_front();
|
||||
entries.emplace_back(now, message);
|
||||
if (!problem)
|
||||
|
||||
@@ -108,6 +108,19 @@ void validateSettings(const Settings &s)
|
||||
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.autoReplacements.size() > 10000)
|
||||
throw std::runtime_error("Too many automatic replacements");
|
||||
for (const auto &[from, to] : s.autoReplacements)
|
||||
{
|
||||
bool valid = false;
|
||||
for (auto alphabet : {L"abcdefghijklmnopqrstuvwxyz", L"абвгґдеєжзиіїйклмнопрстуфхцчшщьюя",
|
||||
L"абвгдеёжзийклмнопрстуфхцчшщъыьэюя"})
|
||||
valid |= !from.empty() && !to.empty() && from.size() <= 32 && to.size() <= 32 &&
|
||||
from == lower(from) && to == lower(to) && from != to &&
|
||||
from.find_first_not_of(alphabet) == from.npos && to.find_first_not_of(alphabet) == to.npos;
|
||||
if (!valid)
|
||||
throw std::runtime_error("Replacement pairs must be different lowercase words of 1-32 letters in the same language");
|
||||
}
|
||||
for (const auto &[language, words] : s.autoWords)
|
||||
{
|
||||
std::wstring alphabet = language == L"en" ? L"abcdefghijklmnopqrstuvwxyz" :
|
||||
@@ -172,6 +185,15 @@ Settings decodeSettings(const Bytes &bytes, const std::vector<Layout> &layouts)
|
||||
s.elevatedAutostart = j.value("autostartAsAdministrator", false);
|
||||
s.awake = j.value("preventScreenLock", false);
|
||||
s.autoCorrect = j.value("autoCorrectLayout", false);
|
||||
s.autoReplace = j.value("autoCorrectReplace", false);
|
||||
s.autoTypos = j.value("autoCorrectTypos", false);
|
||||
if (j.contains("autoCorrectReplacements"))
|
||||
for (auto &[from, to] : j.at("autoCorrectReplacements").items())
|
||||
{
|
||||
auto key = lower(wide(from));
|
||||
if (!s.autoReplacements.emplace(key, lower(wide(to.get<std::string>()))).second)
|
||||
throw std::runtime_error("Duplicate automatic replacement");
|
||||
}
|
||||
s.autoMaxRank = j.value("autoCorrectMaxRank", 0);
|
||||
s.autoMinLength = j.value("autoCorrectMinLength", 3);
|
||||
s.autoPrefixChars = str(j, "autoCorrectPrefixChars", s.autoPrefixChars);
|
||||
@@ -268,6 +290,11 @@ Bytes encodeSettings(const Settings &s)
|
||||
for (auto &id : s.cycle)
|
||||
j["layouts"].push_back(utf8(id));
|
||||
j["autoCorrectLayout"] = s.autoCorrect;
|
||||
j["autoCorrectReplace"] = s.autoReplace;
|
||||
j["autoCorrectTypos"] = s.autoTypos;
|
||||
j["autoCorrectReplacements"] = Json::object();
|
||||
for (const auto &[from, to] : s.autoReplacements)
|
||||
j["autoCorrectReplacements"][utf8(from)] = utf8(to);
|
||||
j["autoCorrectMaxRank"] = s.autoMaxRank;
|
||||
j["autoCorrectMinLength"] = s.autoMinLength;
|
||||
j["autoCorrectPrefixChars"] = utf8(s.autoPrefixChars);
|
||||
|
||||
+47
-10
@@ -17,7 +17,7 @@ Json configJson(const SyncConfig &c)
|
||||
{
|
||||
return {{"enabled", c.enabled}, {"provider", utf8(c.provider)}, {"server", utf8(c.server)},
|
||||
{"mount", utf8(c.mount)}, {"path", utf8(c.path)}, {"authMount", utf8(c.authMount)},
|
||||
{"username", utf8(c.username)}};
|
||||
{"username", utf8(c.username)}, {"keepLocalLayouts", c.keepLocalLayouts}};
|
||||
}
|
||||
std::wstring credentialName(const SyncConfig &c)
|
||||
{
|
||||
@@ -154,6 +154,7 @@ SyncConfig loadSyncConfig(const std::filesystem::path &root)
|
||||
if (!syncVisible(root)) return c;
|
||||
auto j = parse(readFile(root / L"sync.json", 16384));
|
||||
c.enabled = j.value("enabled", false);
|
||||
c.keepLocalLayouts = j.value("keepLocalLayouts", false);
|
||||
c.provider = wide(j.value("provider", "openbao"));
|
||||
c.server = wide(j.value("server", ""));
|
||||
c.mount = wide(j.value("mount", "kswitcher"));
|
||||
@@ -221,30 +222,50 @@ Bytes sharedSyncSettings(const Settings &s)
|
||||
"historyEnabled", "maxItems", "maxTotalMegabytes", "maxImageMegabytes", "writeLog", "preventScreenLock"}) j.erase(key);
|
||||
return bytes(canonicalProfile(j));
|
||||
}
|
||||
Settings restoreSyncSettings(const Bytes &shared, const Settings &local, const std::vector<Layout> &layouts)
|
||||
Settings restoreSyncSettings(const Bytes &shared, const Settings &local, const std::vector<Layout> &layouts, bool keepLocalLayouts)
|
||||
{
|
||||
auto j = parse(shared), original = parse(encodeSettings(local));
|
||||
if (!j.contains("snippetTombstones")) j["snippetTombstones"] = Json::array();
|
||||
if (!j.contains("autoCorrectReplace")) j["autoCorrectReplace"] = false;
|
||||
if (!j.contains("autoCorrectTypos")) j["autoCorrectTypos"] = false;
|
||||
if (!j.contains("autoCorrectReplacements")) j["autoCorrectReplacements"] = Json::object();
|
||||
auto allowed = parse(sharedSyncSettings(local));
|
||||
if (!j.is_object() || j.value("format", "") != "kswitcher-settings" || j.value("schemaVersion", 0) != 1)
|
||||
throw std::runtime_error("Unsupported synchronized settings format.");
|
||||
for (auto it = j.begin(); it != j.end(); ++it)
|
||||
if (!allowed.contains(it.key())) throw std::runtime_error("Remote profile contains unsupported or local-only fields.");
|
||||
if (j.size() != allowed.size()) throw std::runtime_error("Remote profile is incomplete.");
|
||||
// decodeSettings intentionally ignores hotkeys for missing layouts during import.
|
||||
// Sync must stop instead, otherwise the next upload would lose those bindings.
|
||||
std::set<std::string> installed;
|
||||
for (const auto &layout : layouts) installed.insert(utf8(layout.id));
|
||||
std::set<std::string> missing;
|
||||
for (const auto &id : j.at("layouts"))
|
||||
if (!installed.contains(id.get<std::string>())) throw std::runtime_error("Install the remote profile's keyboard layouts before syncing.");
|
||||
if (!installed.contains(id.get<std::string>())) missing.insert(id.get<std::string>());
|
||||
for (const auto &key : j.at("layoutHotkeys"))
|
||||
if (!installed.contains(key.at("layoutId").get<std::string>())) throw std::runtime_error("Install the remote profile's keyboard layouts before syncing.");
|
||||
if (!installed.contains(key.at("layoutId").get<std::string>())) missing.insert(key.at("layoutId").get<std::string>());
|
||||
if (!keepLocalLayouts && !missing.empty())
|
||||
{
|
||||
std::wstring message = L"Missing keyboard layouts on this PC: ";
|
||||
for (const auto &id : missing)
|
||||
{
|
||||
auto code = id.starts_with("win:") ? id.substr(4) : id;
|
||||
wchar_t language[128]{};
|
||||
if (code.size() == 8 && code.find_first_not_of("0123456789abcdefABCDEF") == code.npos)
|
||||
GetLocaleInfoW(MAKELCID(LOWORD(std::stoul(code, nullptr, 16)), SORT_DEFAULT), LOCALE_SENGLISHLANGUAGENAME,
|
||||
language, 128);
|
||||
if (message.back() != L' ') message += L"; ";
|
||||
message += (language[0] ? std::wstring(language) : L"Unknown layout") + L" [" + wide(id) + L"]";
|
||||
}
|
||||
message += L". Install these exact layouts, or enable 'Keep this PC's layouts' below and sync other settings.";
|
||||
throw std::runtime_error(utf8(message));
|
||||
}
|
||||
if (keepLocalLayouts)
|
||||
for (auto key : {"layouts", "layoutHotkeys"}) j[key] = original.at(key);
|
||||
original.update(j);
|
||||
return decodeSettings(bytes(original), layouts);
|
||||
}
|
||||
#include "sync_merge.inl"
|
||||
SyncResult synchronize(SyncProvider &provider, const Bytes &base, const Bytes &local, SyncAction action,
|
||||
const std::function<void(const Bytes &)> &validate, const std::vector<SyncChoice> &choices)
|
||||
const std::function<void(const Bytes &)> &validate, const std::vector<SyncChoice> &choices, bool keepLocalLayouts)
|
||||
{
|
||||
if (local.size() > Limit) throw std::runtime_error("Settings exceed the sync size limit.");
|
||||
if (validate) validate(local);
|
||||
@@ -255,11 +276,25 @@ SyncResult synchronize(SyncProvider &provider, const Bytes &base, const Bytes &l
|
||||
return {true, local, provider.write(local, 0), L"Uploaded new profile."};
|
||||
}
|
||||
if (validate && action != SyncAction::Upload) validate(remote.settings);
|
||||
auto mergeLocal = local, mergeBase = base;
|
||||
if (keepLocalLayouts)
|
||||
{
|
||||
// Never upload a locally filtered projection over another PC's layout setup.
|
||||
const auto remoteJson = parse(remote.settings);
|
||||
auto preserve = [&](const Bytes &profile)
|
||||
{
|
||||
auto j = parse(profile);
|
||||
for (auto key : {"layouts", "layoutHotkeys"}) j[key] = remoteJson.at(key);
|
||||
return bytes(j);
|
||||
};
|
||||
mergeLocal = preserve(local);
|
||||
if (!base.empty()) mergeBase = preserve(base);
|
||||
}
|
||||
SyncMerge merged;
|
||||
if (action == SyncAction::Automatic) merged = mergeSyncProfile(base, local, remote.settings, choices);
|
||||
if (action == SyncAction::Automatic) merged = mergeSyncProfile(mergeBase, mergeLocal, remote.settings, choices);
|
||||
else
|
||||
{
|
||||
auto selected = bytes(normalizeProfile(parse(action == SyncAction::Download ? remote.settings : local)));
|
||||
auto selected = bytes(normalizeProfile(parse(action == SyncAction::Download ? remote.settings : mergeLocal)));
|
||||
merged = {selected, selected, selected, {}};
|
||||
}
|
||||
if (validate) { validate(merged.local); validate(merged.remote); }
|
||||
@@ -308,7 +343,9 @@ Bytes loadSyncBase(const std::filesystem::path &root, const SyncConfig &c)
|
||||
auto path = root / L"sync-base.bin";
|
||||
if (!std::filesystem::exists(path)) return {};
|
||||
auto j = parse(unprotect(readFile(path, Limit + 65536)));
|
||||
return j.at("connection") == configJson(c) ? bytes(j.at("settings")) : Bytes{};
|
||||
auto connection = j.at("connection");
|
||||
if (!connection.contains("keepLocalLayouts")) connection["keepLocalLayouts"] = false;
|
||||
return connection == configJson(c) ? bytes(j.at("settings")) : Bytes{};
|
||||
}
|
||||
void saveSyncBase(const std::filesystem::path &root, const SyncConfig &c, const Bytes &base)
|
||||
{
|
||||
|
||||
+4
-2
@@ -7,6 +7,7 @@ namespace ks
|
||||
struct SyncConfig
|
||||
{
|
||||
bool enabled{};
|
||||
bool keepLocalLayouts{};
|
||||
std::wstring provider = L"openbao", server, mount = L"kswitcher",
|
||||
path = L"personal/settings", authMount = L"userpass", username;
|
||||
bool operator==(const SyncConfig &) const = default;
|
||||
@@ -52,7 +53,7 @@ SyncMerge mergeSyncProfile(const Bytes &base, const Bytes &local, const Bytes &r
|
||||
const std::vector<SyncChoice> &choices = {});
|
||||
Bytes mergeSyncSettings(const Bytes &base, const Bytes &local, const Bytes &remote);
|
||||
Bytes sharedSyncSettings(const Settings &);
|
||||
Settings restoreSyncSettings(const Bytes &, const Settings &, const std::vector<Layout> &);
|
||||
Settings restoreSyncSettings(const Bytes &, const Settings &, const std::vector<Layout> &, bool keepLocalLayouts = false);
|
||||
enum class SyncAction { Automatic, Download, Upload };
|
||||
struct SyncResult
|
||||
{
|
||||
@@ -64,7 +65,8 @@ struct SyncResult
|
||||
std::vector<SyncConflict> conflicts;
|
||||
};
|
||||
SyncResult synchronize(SyncProvider &, const Bytes &base, const Bytes &local, SyncAction,
|
||||
const std::function<void(const Bytes &)> &validate = {}, const std::vector<SyncChoice> &choices = {});
|
||||
const std::function<void(const Bytes &)> &validate = {}, const std::vector<SyncChoice> &choices = {},
|
||||
bool keepLocalLayouts = false);
|
||||
Bytes loadSyncBase(const std::filesystem::path &, const SyncConfig &);
|
||||
void saveSyncBase(const std::filesystem::path &, const SyncConfig &, const Bytes &);
|
||||
}
|
||||
|
||||
+12
-2
@@ -4,6 +4,9 @@ namespace
|
||||
{
|
||||
Json normalizeProfile(Json j)
|
||||
{
|
||||
if (!j.contains("autoCorrectReplace")) j["autoCorrectReplace"] = false;
|
||||
if (!j.contains("autoCorrectTypos")) j["autoCorrectTypos"] = false;
|
||||
if (!j.contains("autoCorrectReplacements")) j["autoCorrectReplacements"] = Json::object();
|
||||
if (!j.contains("snippetTombstones")) j["snippetTombstones"] = Json::array();
|
||||
for (auto &sn : j.at("snippets"))
|
||||
if (!sn.contains("id")) sn["id"] = utf8(snippetId({wide(sn.at("trigger").get<std::string>()), L""}));
|
||||
@@ -46,6 +49,10 @@ Json treeProfile(const Json &source)
|
||||
}
|
||||
Json flatProfile(Json j)
|
||||
{
|
||||
auto &replacements = j["autoCorrectReplacements"];
|
||||
for (auto it = replacements.begin(); it != replacements.end();)
|
||||
if (it.value().is_null()) it = replacements.erase(it);
|
||||
else ++it;
|
||||
Json snippets = Json::array(), deleted = Json::array();
|
||||
for (const auto &[id, item] : j.at("snippets").items())
|
||||
{
|
||||
@@ -90,6 +97,8 @@ std::wstring fieldLabel(const std::string &key)
|
||||
{"convertLastWord", L"Convert recent word"}, {"switchSystemLayout", L"Switch system layout"},
|
||||
{"autoCorrectMinLength", L"Minimum word length"}, {"autoCorrectMaxRank", L"Maximum word rank"},
|
||||
{"autoCorrectLayout", L"Automatic layout correction"}, {"autoCorrectLanguages", L"Dictionary languages"},
|
||||
{"autoCorrectReplace", L"Custom automatic replacements"}, {"autoCorrectTypos", L"Dictionary typo correction"},
|
||||
{"autoCorrectReplacements", L"Replacement pairs"},
|
||||
{"autoCorrectPrefixChars", L"Punctuation before words"}, {"autoCorrectSuffixChars", L"Punctuation after words"},
|
||||
{"screenshotAutoOpenEditor", L"Open screenshot editor"}, {"tesseractLanguages", L"OCR languages"},
|
||||
{"trigger", L"Trigger"}, {"text", L"Text"}};
|
||||
@@ -167,8 +176,9 @@ void collisionPaths(const Json &j, std::set<std::string> &paths)
|
||||
SyncMerge mergeSyncProfile(const Bytes &base, const Bytes &local, const Bytes &remote, const std::vector<SyncChoice> &choices)
|
||||
{
|
||||
auto l = treeProfile(parse(local)), r = treeProfile(parse(remote));
|
||||
if (base.empty() && l != r) throw std::runtime_error("First connection: choose Download remote or Upload local.");
|
||||
auto b = base.empty() ? l : treeProfile(parse(base));
|
||||
// With no common history, union independent entries; differing scalar values
|
||||
// remain on their respective devices until the user resolves the conflict.
|
||||
auto b = base.empty() ? Json::object() : treeProfile(parse(base));
|
||||
SyncMerge result;
|
||||
auto merged = mergeNode(b, l, r, "", L"", choices, result.conflicts, {});
|
||||
std::set<std::string> collisions;
|
||||
|
||||
+39
-7
@@ -1112,6 +1112,25 @@ LRESULT CALLBACK autoOptionsProc(HWND w, UINT msg, WPARAM wp, LPARAM lp)
|
||||
draft.autoExcluded = parseCommaList(value(w, 404));
|
||||
draft.autoPrefixChars = value(w, 408);
|
||||
draft.autoSuffixChars = value(w, 409);
|
||||
draft.autoReplace = checked(w, 414);
|
||||
draft.autoTypos = checked(w, 415);
|
||||
draft.autoReplacements.clear();
|
||||
std::wistringstream replacements(lower(value(w, 416)));
|
||||
std::wstring line;
|
||||
auto trim = [](std::wstring text)
|
||||
{
|
||||
auto first = text.find_first_not_of(L" \t\r\n");
|
||||
return first == text.npos ? std::wstring{} : text.substr(first, text.find_last_not_of(L" \t\r\n") - first + 1);
|
||||
};
|
||||
while (std::getline(replacements, line))
|
||||
{
|
||||
if (trim(line).empty()) continue;
|
||||
auto equals = line.find(L'=');
|
||||
if (equals == line.npos)
|
||||
throw std::runtime_error("Enter one replacement per line: typo=correction");
|
||||
if (!draft.autoReplacements.emplace(trim(line.substr(0, equals)), trim(line.substr(equals + 1))).second)
|
||||
throw std::runtime_error("Each replacement typo must occur only once");
|
||||
}
|
||||
if (draft.autoLanguages.size() < 2)
|
||||
throw std::runtime_error("Select at least two languages");
|
||||
validateSettings(draft);
|
||||
@@ -1123,6 +1142,9 @@ LRESULT CALLBACK autoOptionsProc(HWND w, UINT msg, WPARAM wp, LPARAM lp)
|
||||
view->settings->draft.autoMinLength = draft.autoMinLength;
|
||||
view->settings->draft.autoPrefixChars = draft.autoPrefixChars;
|
||||
view->settings->draft.autoSuffixChars = draft.autoSuffixChars;
|
||||
view->settings->draft.autoReplace = draft.autoReplace;
|
||||
view->settings->draft.autoTypos = draft.autoTypos;
|
||||
view->settings->draft.autoReplacements = std::move(draft.autoReplacements);
|
||||
DestroyWindow(w);
|
||||
}
|
||||
catch (const std::exception &e)
|
||||
@@ -1153,8 +1175,8 @@ LRESULT CALLBACK autoOptionsProc(HWND w, UINT msg, WPARAM wp, LPARAM lp)
|
||||
}
|
||||
void showAutoOptions(HWND parent, SettingsView *v)
|
||||
{
|
||||
HWND w = makeWindow(*v->app, L"KSwitcher.AutoOptions", L"Automatic layout correction", autoOptionsProc,
|
||||
560, 680);
|
||||
HWND w = makeWindow(*v->app, L"KSwitcher.AutoOptions", L"Automatic correction", autoOptionsProc,
|
||||
560, 880);
|
||||
windowLayoutState(w)->autoOptions = true;
|
||||
SetWindowLongPtrW(w, GWLP_HWNDPARENT, (LONG_PTR)parent);
|
||||
SetWindowLongPtrW(w, GWLP_USERDATA, (LONG_PTR) new AutoOptionsView{parent, v});
|
||||
@@ -1192,10 +1214,20 @@ void showAutoOptions(HWND parent, SettingsView *v)
|
||||
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);
|
||||
checkbox(w, L"Use custom &replacements (take priority over dictionaries)", 414, 18, 562, 524);
|
||||
check(w, 414, v->draft.autoReplace);
|
||||
label(w, L"One pair per line: typo=correction (same language, letters only)", 18, 592, 525);
|
||||
edit(w, 416, 18, 618, 524, 70, ES_MULTILINE | WS_VSCROLL | ES_AUTOVSCROLL | ES_WANTRETURN);
|
||||
std::wstring pairs;
|
||||
for (const auto &[from, to] : v->draft.autoReplacements) pairs += from + L"=" + to + L"\r\n";
|
||||
value(w, 416, pairs);
|
||||
checkbox(w, L"Correct spelling &typos using dictionaries (experimental)", 415, 18, 700, 524);
|
||||
check(w, 415, v->draft.autoTypos);
|
||||
label(w, L"Single edits, 4+ letters; ambiguous words are left unchanged.", 18, 730, 525);
|
||||
button(w, L"Open dictionary &folder", 407, 18, 762, 220);
|
||||
label(w, L"Click OK, then Save in Settings. Enable automatic correction there.", 18, 800, 525);
|
||||
button(w, L"&OK", IDOK, 326, 832, 100);
|
||||
button(w, L"&Cancel", IDCANCEL, 440, 832, 100);
|
||||
RECT initial{};
|
||||
GetWindowRect(w, &initial);
|
||||
initial.right += scale(w, 200);
|
||||
@@ -1672,7 +1704,7 @@ void showSettings(App &app, bool visible)
|
||||
hotkeyEdit(app, w, 210 + i, 540, 44 + i * 30, 220);
|
||||
}
|
||||
checkbox(w, L"Open editor after screen capt&ure", 209, 420, 224, 340);
|
||||
checkbox(w, L"Automatically correct layout (e&xperimental)", 207, 18, 210, 395);
|
||||
checkbox(w, L"Automatic correction (e&xperimental)", 207, 18, 210, 395);
|
||||
label(w, L"Layout cycle — top to bottom", 18, 240, 400);
|
||||
control(w, L"LISTBOX", L"", WS_TABSTOP | WS_VSCROLL | LBS_NOTIFY | LBS_NOINTEGRALHEIGHT | WS_BORDER, 220,
|
||||
18, 262, 470, 172);
|
||||
|
||||
+52
-11
@@ -1,9 +1,29 @@
|
||||
#include "ui_sync_conflicts.inl"
|
||||
struct SyncView { HWND parent; App *app; };
|
||||
void updateSyncControls(HWND w, App &app)
|
||||
{
|
||||
bool enabled = checked(w, 601);
|
||||
bool savedEnabled = loadSyncConfig(app.data).enabled;
|
||||
for (int id : {602, 603, 604, 605, 606, 607, 610, 611, 613})
|
||||
EnableWindow(GetDlgItem(w, id), enabled);
|
||||
EnableWindow(GetDlgItem(w, 612), enabled && savedEnabled);
|
||||
EnableWindow(GetDlgItem(w, IDOK), enabled || savedEnabled);
|
||||
value(w, IDOK, enabled ? L"&Save / Sign in / Sync" : L"&Save");
|
||||
std::wstring status;
|
||||
if (!enabled)
|
||||
status = savedEnabled ? L"Click Save to disable synchronization." :
|
||||
L"Synchronization is disabled. Enable it above to configure and sync.";
|
||||
else if (!savedEnabled)
|
||||
status = L"Configure the connection, then click Save / Sign in / Sync to enable synchronization.";
|
||||
else
|
||||
status = app.syncCommand();
|
||||
if (value(w, 608) != status) value(w, 608, status);
|
||||
}
|
||||
SyncConfig syncFields(HWND w)
|
||||
{
|
||||
SyncConfig c;
|
||||
c.enabled = checked(w, 601);
|
||||
c.keepLocalLayouts = checked(w, 613);
|
||||
c.server = value(w, 602);
|
||||
while (!c.server.empty() && c.server.back() == L'/') c.server.pop_back();
|
||||
c.username = lower(value(w, 603)); c.mount = value(w, 605);
|
||||
@@ -19,12 +39,19 @@ LRESULT CALLBACK syncProc(HWND w, UINT msg, WPARAM wp, LPARAM lp)
|
||||
{
|
||||
if (msg == WM_TIMER)
|
||||
{
|
||||
value(w, 608, v->app->syncCommand());
|
||||
updateSyncControls(w, *v->app);
|
||||
return 0;
|
||||
}
|
||||
if (msg == WM_COMMAND)
|
||||
{
|
||||
int id = LOWORD(wp);
|
||||
if (id == 601 && HIWORD(wp) == BN_CLICKED)
|
||||
{
|
||||
updateSyncControls(w, *v->app);
|
||||
return 0;
|
||||
}
|
||||
if ((id == IDOK || id == 610 || id == 611 || id == 612) &&
|
||||
!IsWindowEnabled(GetDlgItem(w, id))) return 0;
|
||||
if (id == 612) { showSyncConflicts(w, *v->app); return 0; }
|
||||
if (id == IDCANCEL) { DestroyWindow(w); return 0; }
|
||||
if (id == 609)
|
||||
@@ -35,18 +62,28 @@ LRESULT CALLBACK syncProc(HWND w, UINT msg, WPARAM wp, LPARAM lp)
|
||||
forgetSyncPassword(c);
|
||||
check(w, 601, false);
|
||||
value(w, 604, L"");
|
||||
updateSyncControls(w, *v->app);
|
||||
value(w, 608, L"Disabled. Saved password removed. An in-flight server request may still finish.");
|
||||
KillTimer(w, 1);
|
||||
return 0;
|
||||
}
|
||||
if (id == IDOK || id == 610 || id == 611)
|
||||
{
|
||||
if (!checked(w, 601))
|
||||
{
|
||||
if (id != IDOK) return 0;
|
||||
auto c = loadSyncConfig(v->app->data);
|
||||
c.enabled = false;
|
||||
saveSyncConfig(v->app->data, c);
|
||||
value(w, 604, L"");
|
||||
updateSyncControls(w, *v->app);
|
||||
return 0;
|
||||
}
|
||||
if (id != IDOK && MessageBoxW(w, id == 610 ?
|
||||
L"Replace this PC's shared settings with the remote profile? A local backup will be saved. Unsaved edits in Settings are not included." :
|
||||
L"Replace the remote profile with this PC's saved settings? OpenBao retains older versions according to its retention policy. Unsaved edits are not included.",
|
||||
L"Resolve synchronization", MB_YESNO | MB_ICONWARNING) != IDYES) return 0;
|
||||
auto c = syncFields(w);
|
||||
if (id != IDOK && !c.enabled) throw std::runtime_error("Enable synchronization first.");
|
||||
if (c.enabled) validateSyncConfig(c);
|
||||
auto password = value(w, 604);
|
||||
try { if (!password.empty()) saveSyncPassword(c, password); }
|
||||
@@ -55,6 +92,7 @@ LRESULT CALLBACK syncProc(HWND w, UINT msg, WPARAM wp, LPARAM lp)
|
||||
value(w, 604, L"");
|
||||
saveSyncConfig(v->app->data, c);
|
||||
value(w, 608, v->app->syncCommand(id == 610 ? 2 : id == 611 ? 3 : 1));
|
||||
updateSyncControls(w, *v->app);
|
||||
SetTimer(w, 1, 1000, nullptr);
|
||||
return 0;
|
||||
}
|
||||
@@ -80,7 +118,7 @@ LRESULT CALLBACK syncProc(HWND w, UINT msg, WPARAM wp, LPARAM lp)
|
||||
void showSyncOptions(HWND parent, App &app)
|
||||
{
|
||||
auto c = loadSyncConfig(app.data);
|
||||
HWND w = makeWindow(app, L"KSwitcher.Sync", L"Settings synchronization - OpenBao", syncProc, 660, 640);
|
||||
HWND w = makeWindow(app, L"KSwitcher.Sync", L"Settings synchronization - OpenBao", syncProc, 660, 672);
|
||||
SetWindowLongPtrW(w, GWLP_HWNDPARENT, (LONG_PTR)parent);
|
||||
SetWindowLongPtrW(w, GWLP_USERDATA, (LONG_PTR)new SyncView{parent, &app});
|
||||
checkbox(w, L"&Enable synchronization with your OpenBao (KV v2)", 601, 18, 18, 620);
|
||||
@@ -93,15 +131,18 @@ void showSyncOptions(HWND parent, App &app)
|
||||
label(w, L"Profile pa&th", 18, 250, 160); edit(w, 606, 190, 246, 450); value(w, 606, c.path);
|
||||
label(w, L"&Login mount", 18, 290, 160); edit(w, 607, 190, 286, 450); value(w, 607, c.authMount);
|
||||
label(w, L"Sync uses saved settings. Close and reopen Settings after downloading changes.", 18, 330, 622);
|
||||
control(w, L"STATIC", L"", 0, 608, 18, 362, 622, 76);
|
||||
edit(w, 608, 18, 362, 622, 76, ES_MULTILINE | ES_READONLY | WS_VSCROLL | ES_AUTOVSCROLL);
|
||||
value(w, 608, app.syncCommand());
|
||||
button(w, L"&Save / Sign in / Sync", IDOK, 18, 452, 200);
|
||||
button(w, L"&Download remote", 610, 230, 452, 195);
|
||||
button(w, L"&Upload local", 611, 437, 452, 203);
|
||||
button(w, L"Resolve &individual conflicts...", 612, 18, 498, 300);
|
||||
label(w, L"Background check: every 10 minutes", 330, 502, 310);
|
||||
button(w, L"Disconnect / &forget password", 609, 18, 550, 290);
|
||||
button(w, L"&Close", IDCANCEL, 540, 550, 100);
|
||||
checkbox(w, L"&Keep this PC's layouts and layout shortcuts (sync other settings)", 613, 18, 446, 622);
|
||||
check(w, 613, c.keepLocalLayouts);
|
||||
button(w, L"&Save / Sign in / Sync", IDOK, 18, 484, 200);
|
||||
button(w, L"&Download remote", 610, 230, 484, 195);
|
||||
button(w, L"&Upload local", 611, 437, 484, 203);
|
||||
button(w, L"Resolve &individual conflicts...", 612, 18, 530, 300);
|
||||
label(w, L"Background check: every 10 minutes", 330, 534, 310);
|
||||
button(w, L"Disconnect / &forget password", 609, 18, 582, 290);
|
||||
button(w, L"&Close", IDCANCEL, 540, 582, 100);
|
||||
updateSyncControls(w, app);
|
||||
EnableWindow(parent, FALSE);
|
||||
SetTimer(w, 1, 1000, nullptr);
|
||||
showRequestedWindow(w); SetForegroundWindow(w);
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@
|
||||
#include "platform.h"
|
||||
namespace ks
|
||||
{
|
||||
inline constexpr wchar_t AppVersion[] = L"0.6.27";
|
||||
inline constexpr wchar_t AppVersion[] = L"0.6.28";
|
||||
inline constexpr UINT UpdateMessage = WM_APP + 7;
|
||||
inline constexpr UINT UpdateCheckIntervalMs = 6 * 60 * 60 * 1000;
|
||||
inline constexpr UINT UpdateNotificationRetryMs = 5 * 60 * 1000;
|
||||
|
||||
Reference in New Issue
Block a user