Release v0.6.61: configure languages per application
This commit is contained in:
1 parent
011664cc53
commit
5d96abb83e
15 files changed
+456
-51
No files matched your search
+23
-8
@@ -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
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
@@ -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
@@ -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);
|
||||
|
||||
@@ -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
@@ -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;
|
||||
|
||||
Reference in new issue
Block a user