Release v0.6.61: configure languages per application

This commit is contained in:
agrynco committed 2026-11-09 23:24:42 +02:00
1 parent 011664cc53
commit 5d96abb83e
15 files changed
+456 -51

No files matched your search

+23 -8
View File
@@ -986,21 +986,21 @@ void App::refreshChatContext()
HWND target = GetForegroundWindow();
chatWindow = target;
activeChatTitle = chatTitle(target);
activeChat = settings.chatCycles.empty() ? L"" : windowChatKey(target);
activeChat = windowLanguageProfileKey(settings, target);
}
void App::switchChatLayout(bool reverse)
{
HWND target = chatWindow;
auto key = activeChat;
if (!target || target != GetForegroundWindow() || key.empty() || windowChatKey(target) != key ||
!settings.chatCycles.contains(key) || busy.exchange(true)) return;
if (!target || target != GetForegroundWindow() || key.empty() ||
windowLanguageProfileKey(settings, target) != key || busy.exchange(true)) return;
auto cycle = chatCycle(settings, key, layouts);
auto available = layouts;
resetInput();
enqueue([this, target, key, cycle, available, reverse]
enqueue([this, target, key, cycle, available, reverse, profileSettings = settings]
{
struct Reset { std::atomic<bool> &busy; ~Reset() { busy = false; } } reset{busy};
auto valid = [&] { return GetForegroundWindow() == target && windowChatKey(target) == key; };
auto valid = [&] { return GetForegroundWindow() == target && windowLanguageProfileKey(profileSettings, target) == key; };
if (key.starts_with(L"telegram-web\n") && readChromeChat(target) != key) return;
if (!valid()) return;
auto next = nextChatLayout(cycle, available, windowLayout(target), reverse);
@@ -1058,11 +1058,11 @@ bool App::handleKeyboard(const KBDLLHOOKSTRUCT &key, bool down, UINT mods)
}
}
bool chatEnabled = false;
if (down && ChatSwitchHook::candidate(key.vkCode, mods) && !settings.chatCycles.empty() &&
if (down && ChatSwitchHook::candidate(key.vkCode, mods) && (!settings.chatCycles.empty() || !settings.appLanguageSets.empty()) &&
!isHotkey(key.vkCode, mods))
{
refreshChatContext();
chatEnabled = !activeChat.empty() && settings.chatCycles.contains(activeChat);
chatEnabled = !activeChat.empty();
}
const int chatAction = chatSwitchHook.update(key.vkCode, down, mods, chatEnabled);
if (chatAction)
@@ -1314,7 +1314,7 @@ void App::transform(int command, int snippet)
} reset{busy, window, preDeferred};
auto effectiveChat = requestedChat;
if (lower(processName(target)) == L"chrome") effectiveChat = readChromeChat(target);
config.cycle = chatCycle(config, effectiveChat, available);
config.cycle = chatCycle(config, languageProfileKey(config, target, effectiveChat), available);
auto start = GetTickCount64();
std::wstring reason = L"cancelled-before-selection";
std::wstring text, result, expected, actual, verification = L"not-attempted";
@@ -2294,6 +2294,20 @@ LRESULT CALLBACK App::windowProc(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp)
if(app->lanService&&app->lanService->tickInput(!app->busy&&!app->suspended&&!app->hotkeyCapture))app->resetLanInput();
return 0;
case WM_TIMER:
if (wp == 8)
{
if (app->settings.appLanguageSets.empty() || app->busy || app->installingUpdate) return 0;
app->refreshChatContext();
if (!app->activeChat.starts_with(L"app\n")) return 0;
auto cycle = chatCycle(app->settings, app->activeChat, app->layouts);
auto current = windowLayout(app->chatWindow);
bool allowed = std::any_of(app->layouts.begin(), app->layouts.end(), [&](const Layout &layout) {
return layout.handle == current && std::find(cycle.begin(), cycle.end(), layout.id) != cycle.end();
});
if (!allowed) app->switchChatLayout(false);
return 0;
}
if (wp == 7)
{
if (app->lanService)
@@ -2452,6 +2466,7 @@ int App::execute()
if (lanService && lanService->inputEnabled()) SetTimer(window, 7, 10, nullptr);
SetTimer(window, 1, 60000, nullptr);
if (!smoke) SetTimer(window, 6, 1000, nullptr);
if (!smoke) SetTimer(window, 8, 250, nullptr);
SetThreadExecutionState(ES_CONTINUOUS |
(settings.awake ? (ES_DISPLAY_REQUIRED | ES_SYSTEM_REQUIRED) : 0));
if (!registrationWarning.empty())
+32 -2
View File
@@ -92,13 +92,43 @@ inline std::wstring windowChatKey(HWND window)
if (window && lower(processName(window)) == L"chrome") return cachedChromeChat(window);
return window ? chatKey(processName(window), chatTitle(window)) : L"";
}
inline std::wstring applicationName(std::wstring process)
{
process = lower(process);
if (process.ends_with(L".exe")) process.resize(process.size() - 4);
return process;
}
inline const AppLanguageSet *applicationLanguageSet(const Settings &settings, const std::wstring &process)
{
auto name = applicationName(process);
if (name.empty()) return nullptr;
for (const auto &[label, set] : settings.appLanguageSets)
for (const auto &app : set.applications)
if (applicationName(app) == name) return &set;
return nullptr;
}
inline std::wstring languageProfileKey(const Settings &settings, HWND window, const std::wstring &chat)
{
if (!chat.empty() && settings.chatCycles.contains(chat)) return chat;
if (settings.appLanguageSets.empty()) return {};
auto app = applicationName(processName(window));
return applicationLanguageSet(settings, app) ? L"app\n" + app : L"";
}
inline std::wstring windowLanguageProfileKey(const Settings &settings, HWND window)
{
return window ? languageProfileKey(settings, window, settings.chatCycles.empty() ? L"" : windowChatKey(window)) : L"";
}
inline std::vector<std::wstring> chatCycle(const Settings &settings, const std::wstring &key,
const std::vector<Layout> &layouts)
{
auto found = settings.chatCycles.find(key);
if (key.empty() || found == settings.chatCycles.end()) return settings.cycle;
const std::vector<std::wstring> *cycle = found == settings.chatCycles.end() ? nullptr : &found->second;
if (key.starts_with(L"app\n"))
if (auto set = applicationLanguageSet(settings, key.substr(4))) cycle = &set->languages;
if (!cycle) return settings.cycle;
std::vector<std::wstring> result;
for (const auto &id : found->second)
for (const auto &id : *cycle)
if (std::any_of(layouts.begin(), layouts.end(), [&](const Layout &l) { return l.id == id; }))
result.push_back(id);
return result.empty() ? settings.cycle : result;
+6
View File
@@ -53,6 +53,11 @@ struct Hotkey
static std::optional<Hotkey> parse(std::wstring);
std::wstring format() const;
};
struct AppLanguageSet
{
std::vector<std::wstring> languages, applications;
bool operator==(const AppLanguageSet &) const = default;
};
struct Settings
{
bool recent = true, switchLayout = true, history = true, autostart = false, awake = false;
@@ -82,6 +87,7 @@ struct Settings
std::vector<std::wstring> cycle, direct, directIds;
// Exact application + visible chat name; an absent entry uses the global cycle.
std::map<std::wstring, std::vector<std::wstring>> chatCycles;
std::map<std::wstring, AppLanguageSet> appLanguageSets;
std::vector<std::wstring> excluded{L"KeePass", L"KeePassXC", L"1Password", L"Bitwarden"};
std::vector<Snippet> snippets;
std::vector<std::wstring> snippetTombstones;
+48
View File
@@ -58,9 +58,35 @@ bool validId(const std::wstring &id)
{
return !id.empty() && id.size() < 80 && id.find_first_not_of(L"0123456789abcdef-") == id.npos;
}
void validateLanguageCycle(const std::vector<std::wstring> &cycle)
{
if (cycle.empty() || cycle.size() > 64) throw std::runtime_error("Select between 1 and 64 languages.");
std::set<std::wstring> seen;
for (const auto &id : cycle)
if (id.size() != 12 || !id.starts_with(L"win:") ||
id.find_first_not_of(L"0123456789abcdefABCDEF", 4) != id.npos || !seen.insert(lower(id)).second)
throw std::runtime_error("Invalid or duplicate language layout.");
}
} // namespace
void validateSettings(const Settings &s)
{
if (s.appLanguageSets.size() > 1000) throw std::runtime_error("Too many application language sets");
std::set<std::wstring> assigned;
for (const auto &[name, set] : s.appLanguageSets)
{
if (name.empty() || name.size() > 128 || name.find_first_of(L"\r\n") != name.npos)
throw std::runtime_error("Enter a language set name (up to 128 characters).");
if (set.applications.size() > 1000) throw std::runtime_error("Too many applications in a language set");
for (auto app : set.applications)
{
app = lower(app);
if (app.ends_with(L".exe")) app.resize(app.size() - 4);
if (app.empty() || app.size() > 260 || app.find_first_of(L"\\/:*?\"<>|\r\n") != app.npos)
throw std::runtime_error("Use an application executable name, not a path.");
if (!assigned.insert(app).second) throw std::runtime_error("An application can belong to only one language set.");
}
validateLanguageCycle(set.languages);
}
if (s.chatCycles.size() > 1000) throw std::runtime_error("Too many chat language profiles");
for (const auto &[key, cycle] : s.chatCycles)
{
@@ -263,6 +289,19 @@ Settings decodeSettings(const Bytes &bytes, const std::vector<Layout> &layouts)
s.cycle.push_back(w);
}
}
if (j.contains("appLanguageSets"))
{
if (!j.at("appLanguageSets").is_object()) throw std::runtime_error("Invalid application language sets");
for (const auto &[name, item] : j.at("appLanguageSets").items())
{
AppLanguageSet set;
if (!item.at("languages").is_array() || !item.at("applications").is_array())
throw std::runtime_error("Invalid application language set");
for (const auto &id : item.at("languages")) set.languages.push_back(wide(id.get<std::string>()));
for (const auto &app : item.at("applications")) set.applications.push_back(wide(app.get<std::string>()));
s.appLanguageSets[wide(name)] = std::move(set);
}
}
if (j.contains("chatCycles"))
{
if (!j.at("chatCycles").is_object()) throw std::runtime_error("Invalid chat profiles object");
@@ -335,6 +374,15 @@ Bytes encodeSettings(const Settings &s)
{"screenshotHotkey", utf8(s.screenshot)},
{"screenshotAutoOpenEditor", s.screenshotAutoOpenEditor}};
j["layouts"] = Json::array();
j["appLanguageSets"] = Json::object();
for (const auto &[name, set] : s.appLanguageSets)
{
auto &item = j["appLanguageSets"][utf8(name)];
item["languages"] = Json::array();
item["applications"] = Json::array();
for (const auto &id : set.languages) item["languages"].push_back(utf8(id));
for (const auto &app : set.applications) item["applications"].push_back(utf8(app));
}
j["chatCycles"] = Json::object();
for (const auto &[key, cycle] : s.chatCycles)
for (const auto &id : cycle) j["chatCycles"][utf8(key)].push_back(utf8(id));
+1 -1
View File
@@ -226,7 +226,7 @@ Bytes sharedSyncSettings(const Settings &s)
return a.at("layoutId").get<std::string>() < b.at("layoutId").get<std::string>();
});
for (auto key : {"startWithSystem", "autostartAsAdministrator", "tesseractPath", "useTesseract",
"historyEnabled", "maxItems", "maxTotalMegabytes", "maxImageMegabytes", "writeLog", "preventScreenLock"}) j.erase(key);
"historyEnabled", "maxItems", "maxTotalMegabytes", "maxImageMegabytes", "writeLog", "preventScreenLock", "appLanguageSets"}) j.erase(key);
return bytes(canonicalProfile(j));
}
Settings restoreSyncSettings(const Bytes &shared, const Settings &local, const std::vector<Layout> &layouts, bool keepLocalLayouts)
+40 -18
View File
@@ -1060,12 +1060,27 @@ struct ChatLanguagesView
HWND parent{};
SettingsView *settings{};
std::vector<std::wstring> keys;
Settings draft;
int selection = -1;
bool dirty{};
};
void collectChatLanguages(HWND w, ChatLanguagesView *v)
{
if (!v->dirty || v->selection < 0) return;
std::vector<std::wstring> cycle;
for (size_t i = 0; i < v->settings->order.size(); ++i)
if (SendDlgItemMessageW(w, 711, LB_GETSEL, i, 0) > 0)
cycle.push_back(v->settings->app->layouts[v->settings->order[i]].id);
if (cycle.empty()) throw std::runtime_error("Select at least one language.");
v->draft.chatCycles[v->keys[v->selection]] = std::move(cycle);
v->dirty = false;
}
void fillChatLanguages(HWND w, ChatLanguagesView *v)
{
int index = (int)SendDlgItemMessageW(w, 710, LB_GETCURSEL, 0, 0);
if (index < 0 || index >= (int)v->keys.size()) return;
const auto &draft = v->settings->draft;
v->selection = index;
const auto &draft = v->draft;
auto saved = draft.chatCycles.find(v->keys[index]);
const auto &cycle = saved == draft.chatCycles.end() ? draft.cycle : saved->second;
for (size_t i = 0; i < v->settings->order.size(); ++i)
@@ -1075,7 +1090,7 @@ void fillChatLanguages(HWND w, ChatLanguagesView *v)
std::find(cycle.begin(), cycle.end(), layout.id) != cycle.end(), i);
}
value(w, 712, saved == draft.chatCycles.end() ? L"No profile yet. Select the languages for this chat." :
L"Saved profile. Select languages, or remove it to use the defaults.");
L"Chat profile. Select languages, or remove it to use the defaults.");
EnableWindow(GetDlgItem(w, 713), saved != draft.chatCycles.end());
}
LRESULT CALLBACK chatLanguagesProc(HWND w, UINT msg, WPARAM wp, LPARAM lp)
@@ -1087,25 +1102,29 @@ LRESULT CALLBACK chatLanguagesProc(HWND w, UINT msg, WPARAM wp, LPARAM lp)
if (msg == WM_COMMAND)
{
int id = LOWORD(wp);
if (id == 710 && HIWORD(wp) == LBN_SELCHANGE) { fillChatLanguages(w, v); return 0; }
if (id == 711 && HIWORD(wp) == LBN_SELCHANGE) { v->dirty = true; return 0; }
if (id == 710 && HIWORD(wp) == LBN_SELCHANGE)
{
try { collectChatLanguages(w, v); }
catch (...) { SendDlgItemMessageW(w, 710, LB_SETCURSEL, v->selection, 0); throw; }
fillChatLanguages(w, v);
return 0;
}
if (id == IDOK || id == 713)
{
int selected = (int)SendDlgItemMessageW(w, 710, LB_GETCURSEL, 0, 0);
if (selected < 0 || selected >= (int)v->keys.size())
throw std::runtime_error("Open Telegram, Telegram Web K in Chrome, or Teams, then reopen this dialog. Viber OCR is disabled.");
auto next = v->settings->draft;
if (id == 713) next.chatCycles.erase(v->keys[selected]);
else
throw std::runtime_error("Open a Telegram or Teams chat, then reopen this dialog.");
if (id == 713)
{
std::vector<std::wstring> cycle;
for (size_t i = 0; i < v->settings->order.size(); ++i)
if (SendDlgItemMessageW(w, 711, LB_GETSEL, i, 0) > 0)
cycle.push_back(v->settings->app->layouts[v->settings->order[i]].id);
if (cycle.empty()) throw std::runtime_error("Select at least one language.");
next.chatCycles[v->keys[selected]] = std::move(cycle);
v->draft.chatCycles.erase(v->keys[selected]);
v->dirty = false;
fillChatLanguages(w, v);
return 0;
}
validateSettings(next);
v->settings->draft.chatCycles = std::move(next.chatCycles);
collectChatLanguages(w, v);
validateSettings(v->draft);
v->settings->draft.chatCycles = std::move(v->draft.chatCycles);
DestroyWindow(w);
return 0;
}
@@ -1140,6 +1159,7 @@ void showChatLanguages(HWND parent, SettingsView *settings)
}, (LPARAM)&context);
HWND w = makeWindow(*settings->app, L"KSwitcher.ChatLanguages", L"Languages for a chat", chatLanguagesProc, 640, 524);
auto *v = new ChatLanguagesView{parent, settings, {keys.begin(), keys.end()}};
v->draft = settings->draft;
SetWindowLongPtrW(w, GWLP_HWNDPARENT, (LONG_PTR)parent);
SetWindowLongPtrW(w, GWLP_USERDATA, (LONG_PTR)v);
label(w, L"Telegram / Telegram Web (Chrome) / Teams; Viber inactive", 18, 16, 604);
@@ -1153,12 +1173,12 @@ void showChatLanguages(HWND parent, SettingsView *settings)
}
control(w, L"STATIC", L"Open a chat, then reopen this dialog to add it.", 0, 712, 18, 194, 604, 26);
label(w, L"Click languages to include (saved in the order shown)", 18, 228, 604);
control(w, L"LISTBOX", L"", WS_TABSTOP | WS_VSCROLL | LBS_MULTIPLESEL | LBS_NOINTEGRALHEIGHT | WS_BORDER,
control(w, L"LISTBOX", L"", WS_TABSTOP | WS_VSCROLL | LBS_NOTIFY | LBS_MULTIPLESEL | LBS_NOINTEGRALHEIGHT | WS_BORDER,
711, 18, 256, 604, 132);
for (int i : settings->order)
SendDlgItemMessageW(w, 711, LB_ADDSTRING, 0, (LPARAM)settings->app->layouts[i].name.c_str());
label(w, L"Applies to Alt+Shift, Win+Space and KSwitcher Next layout.", 18, 402, 604);
label(w, L"Matched by chat name. Identical names share a profile. Then Save settings.", 18, 430, 604);
label(w, L"Switch chats freely. Use languages, then Save settings.", 18, 430, 604);
button(w, L"&Remove profile", 713, 18, 476, 170);
button(w, L"&Use languages", IDOK, 300, 476, 170);
button(w, L"&Cancel", IDCANCEL, 486, 476, 136);
@@ -1169,6 +1189,7 @@ void showChatLanguages(HWND parent, SettingsView *settings)
showRequestedWindow(w);
SetFocus(GetDlgItem(w, 710));
}
#include "ui_app_languages.inl"
void collectLayout(HWND w, SettingsView *v)
{
if (v->selection < 0 || v->selection >= (int)v->order.size())
@@ -1723,6 +1744,7 @@ LRESULT CALLBACK settingsProc(HWND w, UINT msg, WPARAM wp, LPARAM lp)
if (id == 257) { showOcrOptions(w, v); return 0; }
if (id == 258) { showSyncOptions(w, *v->app); return 0; }
if (id == 262) { showLan(w, *v->app); return 0; }
if (id == 263) { collectLayout(w, v); showAppLanguages(w, v); return 0; }
if (id == 259) { collectLayout(w, v); showChatLanguages(w, v); return 0; }
if (id == 230)
{
@@ -2153,7 +2175,7 @@ void showSettings(App &app, bool visible)
label(w, L"Backspace / Delete clears it", 454, 270, 230);
button(w, L"Re&fresh layouts", 253, 454, 322, 230);
button(w, L"Languages for a &chat...", 259, 24, 378, 230);
label(w, L"Telegram / Telegram Web (Chrome) / Teams", 272, 382, 412);
button(w, L"Languages for &applications...", 263, 272, 378, 300);
page();
label(w, L"Click a shortcut field and press the desired keys", 24, 76, 660);
+177
View File
@@ -0,0 +1,177 @@
// Included inside ui.cpp's anonymous namespace, alongside the settings editors.
std::wstring trimApplicationText(const std::wstring &text)
{
auto first = text.find_first_not_of(L" \t\r\n");
return first == text.npos ? L"" : text.substr(first, text.find_last_not_of(L" \t\r\n") - first + 1);
}
struct AppLanguagesView
{
HWND parent{};
SettingsView *settings{};
std::map<std::wstring, std::vector<std::wstring>> profiles;
std::vector<std::wstring> applications;
int selection = -1;
bool dirty{};
};
void fillAppLanguages(HWND w, AppLanguagesView *v)
{
bool selected = v->selection >= 0 && v->selection < (int)v->applications.size();
EnableWindow(GetDlgItem(w, 722), selected);
SendDlgItemMessageW(w, 722, LB_SETSEL, FALSE, -1);
if (!selected)
{
EnableWindow(GetDlgItem(w, 725), FALSE);
value(w, 721, L"Select an application, or add its executable name below.");
return;
}
auto found = v->profiles.find(v->applications[v->selection]);
const auto &cycle = found == v->profiles.end() ? v->settings->draft.cycle : found->second;
for (size_t i = 0; i < v->settings->order.size(); ++i)
{
const auto &id = v->settings->app->layouts[v->settings->order[i]].id;
SendDlgItemMessageW(w, 722, LB_SETSEL,
std::find(cycle.begin(), cycle.end(), id) != cycle.end(), i);
}
value(w, 721, found == v->profiles.end() ? L"Using default languages. Click languages to customize this application." :
L"Custom languages for this application. Other applications are unchanged.");
EnableWindow(GetDlgItem(w, 725), found != v->profiles.end());
}
void collectAppLanguages(HWND w, AppLanguagesView *v)
{
if (v->selection < 0 || !v->dirty) return;
std::vector<std::wstring> cycle;
for (size_t i = 0; i < v->settings->order.size(); ++i)
if (SendDlgItemMessageW(w, 722, LB_GETSEL, i, 0) > 0)
cycle.push_back(v->settings->app->layouts[v->settings->order[i]].id);
if (cycle.empty()) throw std::runtime_error("Select at least one language.");
v->profiles[v->applications[v->selection]] = std::move(cycle);
v->dirty = false;
}
LRESULT CALLBACK appLanguagesProc(HWND w, UINT msg, WPARAM wp, LPARAM lp)
{
auto *v = (AppLanguagesView *)GetWindowLongPtrW(w, GWLP_USERDATA);
if (!v) return DefWindowProcW(w, msg, wp, lp);
try
{
if (msg == WM_COMMAND)
{
int id = LOWORD(wp);
if (id == 722 && HIWORD(wp) == LBN_SELCHANGE)
{
v->dirty = true;
EnableWindow(GetDlgItem(w, 725), TRUE);
value(w, 721, L"Custom languages for this application. Other applications are unchanged.");
return 0;
}
if (id == 720 && HIWORD(wp) == LBN_SELCHANGE)
{
int next = (int)SendDlgItemMessageW(w, 720, LB_GETCURSEL, 0, 0);
try { collectAppLanguages(w, v); }
catch (...) { SendDlgItemMessageW(w, 720, LB_SETCURSEL, v->selection, 0); throw; }
v->selection = next;
fillAppLanguages(w, v);
return 0;
}
if (id == 725 && v->selection >= 0)
{
v->profiles.erase(v->applications[v->selection]);
v->dirty = false;
fillAppLanguages(w, v);
return 0;
}
if (id == 727)
{
auto app = applicationName(trimApplicationText(value(w, 726)));
Settings check;
check.appLanguageSets[L"check"] = {{L"win:04090409"}, {app}};
validateSettings(check);
collectAppLanguages(w, v);
auto found = std::find(v->applications.begin(), v->applications.end(), app);
int index = (int)(found - v->applications.begin());
if (found == v->applications.end())
{
v->applications.push_back(app);
SendDlgItemMessageW(w, 720, LB_ADDSTRING, 0, (LPARAM)app.c_str());
}
v->selection = index;
SendDlgItemMessageW(w, 720, LB_SETCURSEL, index, 0);
fillAppLanguages(w, v);
value(w, 726, L"");
SetFocus(GetDlgItem(w, 722));
return 0;
}
if (id == IDOK)
{
collectAppLanguages(w, v);
auto next = v->settings->draft;
next.appLanguageSets.clear();
// Keep the existing on-disk format, but save independent application profiles.
for (const auto &[app, cycle] : v->profiles) next.appLanguageSets[app] = {cycle, {app}};
validateSettings(next);
v->settings->draft.appLanguageSets = std::move(next.appLanguageSets);
DestroyWindow(w);
return 0;
}
if (id == IDCANCEL) { DestroyWindow(w); return 0; }
}
if (msg == WM_CLOSE) { DestroyWindow(w); return 0; }
if (msg == WM_DPICHANGED) { resizeForDpi(w, wp, lp); return 0; }
if (msg == WM_NCDESTROY)
{
EnableWindow(v->parent, TRUE);
SetForegroundWindow(v->parent);
disposeFont(w);
delete v;
SetWindowLongPtrW(w, GWLP_USERDATA, 0);
}
}
catch (const std::exception &e) { error(w, e); }
return DefWindowProcW(w, msg, wp, lp);
}
void showAppLanguages(HWND parent, SettingsView *settings)
{
std::set<std::wstring> apps;
for (const auto &[name, set] : settings->draft.appLanguageSets)
for (const auto &app : set.applications) apps.insert(applicationName(app));
EnumWindows([](HWND candidate, LPARAM context) -> BOOL {
if (IsWindowVisible(candidate) && !chatTitle(candidate).empty())
{
auto name = applicationName(processName(candidate));
if (!name.empty()) ((std::set<std::wstring> *)context)->insert(name);
}
return TRUE;
}, (LPARAM)&apps);
HWND w = makeWindow(*settings->app, L"KSwitcher.AppLanguages", L"Languages for applications", appLanguagesProc, 720, 500);
auto *v = new AppLanguagesView{parent, settings};
for (const auto &[name, set] : settings->draft.appLanguageSets)
for (const auto &app : set.applications) v->profiles[applicationName(app)] = set.languages;
v->applications.assign(apps.begin(), apps.end());
SetWindowLongPtrW(w, GWLP_HWNDPARENT, (LONG_PTR)parent);
SetWindowLongPtrW(w, GWLP_USERDATA, (LONG_PTR)v);
label(w, L"Application", 18, 16, 278);
control(w, L"LISTBOX", L"", WS_TABSTOP | WS_VSCROLL | LBS_NOTIFY | LBS_NOINTEGRALHEIGHT | WS_BORDER,
720, 18, 44, 278, 280);
label(w, L"Allowed languages (click to include)", 316, 16, 386);
control(w, L"LISTBOX", L"", WS_TABSTOP | WS_VSCROLL | LBS_NOTIFY | LBS_MULTIPLESEL | LBS_NOINTEGRALHEIGHT | WS_BORDER,
722, 316, 44, 386, 280);
for (int i : settings->order)
SendDlgItemMessageW(w, 722, LB_ADDSTRING, 0, (LPARAM)settings->app->layouts[i].name.c_str());
for (const auto &app : v->applications) SendDlgItemMessageW(w, 720, LB_ADDSTRING, 0, (LPARAM)app.c_str());
control(w, L"STATIC", L"", 0, 721, 18, 338, 684, 22);
label(w, L"Add an executable name (e.g. Codex.exe)", 18, 372, 684);
edit(w, 726, 18, 400, 526);
button(w, L"&Add application", 727, 558, 398, 144);
label(w, L"Switch applications freely. Use languages, then Save settings. Chat profiles take priority.", 18, 436, 684);
button(w, L"Use &defaults", 725, 18, 466, 170);
button(w, L"&Use languages", IDOK, 414, 466, 144);
button(w, L"&Cancel", IDCANCEL, 572, 466, 130);
if (!v->applications.empty())
{
v->selection = 0;
SendDlgItemMessageW(w, 720, LB_SETCURSEL, 0, 0);
}
fillAppLanguages(w, v);
EnableWindow(parent, FALSE);
showRequestedWindow(w);
SetFocus(GetDlgItem(w, v->selection < 0 ? 726 : 720));
}
+1 -1
View File
@@ -2,7 +2,7 @@
#include "platform.h"
namespace ks
{
inline constexpr wchar_t AppVersion[] = L"0.6.60";
inline constexpr wchar_t AppVersion[] = L"0.6.61";
inline constexpr UINT UpdateMessage = WM_APP + 7;
inline constexpr UINT UpdateCheckIntervalMs = 6 * 60 * 60 * 1000;
inline constexpr UINT UpdateNotificationRetryMs = 5 * 60 * 1000;