Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3761b790ae | ||
|
|
a0e1de40ef | ||
|
|
5d96abb83e |
No files matched your search
+1
-1
@@ -1,5 +1,5 @@
|
||||
cmake_minimum_required(VERSION 3.24)
|
||||
project(KSwitcher VERSION 0.6.60 LANGUAGES CXX RC)
|
||||
project(KSwitcher VERSION 0.6.62 LANGUAGES CXX RC)
|
||||
if(NOT WIN32)
|
||||
message(FATAL_ERROR "KSwitcher uses native Windows APIs")
|
||||
endif()
|
||||
|
||||
@@ -901,3 +901,57 @@ History opens in a separate, short-lived instance of **KSwitcher.exe**. **Esc**
|
||||
Image thumbnails are decoded in **KSwitcher.Screenshot.exe**, returning only bounded preview pixels. Image clipboard backup/restore also uses that helper and encrypted temporary files, with clipboard-sequence checks before restoring. Selection reads avoid loading image formats. Keep all three executables together; no fourth executable is required. Paste allows a brief foreground handoff after history closes while preserving cancellation checks. UI Automation remains in the resident.
|
||||
|
||||
A live development-build test measured **4.37 MiB before history and 4.35 MiB after two open/Esc cycles**, compared with an earlier roughly 8–9 MiB plateau. A same-image clipboard-backup benchmark retained **12.78 MiB before versus 0.05 MiB after**. These measure private working set, not a guaranteed total footprint. Include all helpers when measuring active use: combined memory can be higher while history is open. No forced working-set trimming is used. Existing screenshots and shortcuts remain applicable. The v0.6.38 approach of retaining a hidden history window/RichEdit control is superseded by process termination.
|
||||
|
||||
### Languages for applications
|
||||
|
||||
Open **Settings → Languages → Languages for applications…**. Select an application
|
||||
on the left and click its allowed languages on the right. Every application has
|
||||
an independent selection, even when several applications use the same languages.
|
||||
Existing assignments from the earlier shared-set editor retain their language
|
||||
choices and become independent profiles when this dialog is applied.
|
||||
|
||||
The list includes running applications and saved profiles. Add a closed application
|
||||
by its executable name, such as `Codex.exe`. Matching ignores case and `.exe` and
|
||||
covers all windows of that executable, including all tabs of a browser.
|
||||
To identify an open application, drag **Drag to window** onto its window. An outline
|
||||
shows which window will be selected. Release to select its executable or press **Esc** to cancel.
|
||||
Applications without a custom profile use the default languages; merely viewing
|
||||
an application does not create a profile. **Use defaults** clears only the selected
|
||||
application's custom choice. Switching applications preserves pending edits.
|
||||
Choose **Use languages** once, then **Save** in Settings; **Cancel** discards the
|
||||
dialog's changes. A matching contact profile takes priority over an application
|
||||
profile, which takes priority over the default cycle.
|
||||
|
||||
**Alt+Shift**, **Win+Space**, reverse **Win+Shift+Space** and **Next layout** use the
|
||||
resolved subset. In an application with custom languages, a layout outside the
|
||||
subset is returned to the first available allowed language by a periodic check
|
||||
(250 ms, deferred while KSwitcher is busy). This also handles entering the app
|
||||
with an excluded language active or selecting one via Windows or a direct shortcut.
|
||||
Languages remain installed and visible in Windows' language picker. Unavailable
|
||||
layouts are skipped; if the entire selection is unavailable, the default cycle is
|
||||
used. Profiles stay local to this PC, survive cloud sync and are included in manual
|
||||
settings export/import.
|
||||
|
||||
**Українською.** Відкрий **Settings → Languages → Languages for applications…**.
|
||||
Обери програму зліва та познач дозволені мови справа. Вибір для кожної програми
|
||||
незалежний, навіть якщо в кількох програмах він однаковий. Старі прив'язки спільних
|
||||
наборів зберігають вибрані мови та стають незалежними профілями після застосування
|
||||
цього вікна. У списку є відкриті програми та збережені профілі; закриту програму
|
||||
можна додати за назвою файла, наприклад `Codex.exe`. Регістр і `.exe` не мають
|
||||
значення. Правило охоплює всі вікна програми, зокрема всі вкладки браузера.
|
||||
Щоб визначити відкриту програму, перетягни приціл **Drag to window** на її вікно.
|
||||
Рамка показує, яке вікно буде вибрано. Відпусти приціл, щоб вибрати програму,
|
||||
або натисни **Esc**, щоб скасувати.
|
||||
Без окремого профілю діють загальні мови; простий перегляд програми не створює
|
||||
профіль. **Use defaults** повертає лише вибрану програму до загальних мов.
|
||||
Перехід між програмами зберігає чернетки. **Use languages**, потім **Save**
|
||||
застосовує всі зміни, **Cancel** скасовує зміни вікна. Пріоритет: профіль контакту
|
||||
→ профіль програми → загальний цикл.
|
||||
**Alt+Shift**, **Win+Space**, **Win+Shift+Space** та **Next layout** використовують
|
||||
відповідний вибір. Мову поза профілем програми перевірка кожні 250 мс замінює
|
||||
першою доступною дозволеною мовою (відкладається, коли KSwitcher зайнятий).
|
||||
Це діє і при вході в програму, і після ручного перемикання через Windows чи пряму
|
||||
комбінацію. Мови залишаються встановленими й видимими в меню Windows.
|
||||
Недоступні розкладки пропускаються; якщо недоступні всі вибрані мови, діє
|
||||
загальний цикл. Профілі локальні, зберігаються під час хмарної синхронізації
|
||||
та входять до ручного експорту/імпорту налаштувань.
|
||||
Binary file not shown.
+8
-16
@@ -1,24 +1,16 @@
|
||||
# Latest Windows release
|
||||
|
||||
## v0.6.60 — 2026-11-09
|
||||
## v0.6.62 — 2026-11-10
|
||||
|
||||
## Changes
|
||||
- Fix custom auto-correction pairs such as `ревю=рев'ю`: correction text now accepts straight (`'`), typographic (`’`) and modifier-letter (`ʼ`) apostrophes between letters, both when saving settings and applying replacements.
|
||||
- Preserve the exact apostrophe, initial capitalization, surrounding punctuation and current layout. Multi-word corrections remain supported, with a maximum of 32 characters including spaces and apostrophes.
|
||||
- Use the same correction validator for settings and runtime. Source words still contain only letters; leading/trailing or adjacent apostrophes, apostrophes next to spaces, other punctuation and mixed-language replacements remain invalid. Update the validation message and user documentation.
|
||||
- Retain all v0.6.59 sync, LAN, clipboard, screenshot, snippet and chat-profile behavior. No new setting, shortcut or default.
|
||||
In **Settings → Languages → Languages for applications...**, drag **⊕ Drag to window** onto an open window. A frame follows the window under the crosshair. Release to select the program in the list, or press **Esc** to cancel. If the program is missing from the list, KSwitcher adds it for selection. Pending edits for other applications are preserved.
|
||||
|
||||
## Validation
|
||||
Passed: 349 native checks (including apostrophe validation, correction, capitalization/punctuation and settings roundtrips), 571 input/UI, 829 chat-profile/UI, 132 sync, 205 LAN input, 289 LAN clipboard, 43 LAN pairing and 5 settings-save merge checks; dictionary equivalence for 140,939 words; wheel, StayHere, Utilities, screenshot UI/save/registration, 54 updater checks and updater progress. Release packaging verifies the manifest, all file hashes and the ZIP checksum.
|
||||
Choose the program's allowed layouts, then **Use languages** and **Save**. Application profiles apply to the executable, including all browser tabs in that executable. Contact profiles still take priority.
|
||||
|
||||
|
||||
## Upgrade
|
||||
|
||||
## Upgrade and compatibility
|
||||
Choose **Update** in the tray menu. Legacy v0.3.0–v0.4.1 clients first receive the v0.4.2 compatibility bridge, then the current full release. v0.2.3 and earlier require manual installation.
|
||||
For manual installation, exit KSwitcher, close its configurator and screenshot editors, and extract the entire ZIP into its installation folder. Keep personal `data/`, customized dictionaries and your own tools. Keep the three KSwitcher executables together; StayHere runs independently.
|
||||
|
||||
**Update every synchronizing PC to v0.6.51 or newer before sharing application-specific snippets. Older versions do not understand application restrictions and may treat scoped snippets as global.** Keep a backup before downgrading. Settings with Telegram Web profiles require v0.6.51 or newer. Chat profiles now synchronize from v0.6.53. Update every synchronizing PC to v0.6.53 or newer before syncing: older clients reject the chat-profile field. Synchronization stores application names, visible chat names and language IDs on your configured OpenBao server. No message contents are included.
|
||||
For a manual upgrade, exit KSwitcher and its configurator and screenshot editors, then extract the entire **KSwitcher-win-x64.zip** into its installation folder. Verify it with **SHA256SUMS.txt**. Keep personal data, customized dictionaries and tools. Keep the three KSwitcher executables together.
|
||||
|
||||
**Update every synchronizing PC to v0.6.60 or newer before sharing replacement pairs containing apostrophes. Older clients reject these settings.** Existing letter-only and phrase pairs require no migration.
|
||||
|
||||
Download **KSwitcher-win-x64.zip** with **SHA256SUMS.txt**. The package contains no personal settings, credentials or clipboard history.
|
||||
|
||||
Publish stable `release-vX.Y.Z` tags matching the executable version. Keep `v0.4.2` as the last legacy tag.
|
||||
See the [English Wiki](https://gitea.hrynco.com/hrynco/hrynco-kswitcher/wiki/Home) or [Українська Wiki](https://gitea.hrynco.com/hrynco/hrynco-kswitcher/wiki/Home-uk).
|
||||
@@ -1 +1 @@
|
||||
31e6d5571db106154e3b72391c4f13b856523e602c84a1f8a2935f05c9508854 KSwitcher-win-x64.zip
|
||||
bd1fc311f17fcd26c438449c1ddbb82a30261824396ba18e7f0831f02d719f78 KSwitcher-win-x64.zip
|
||||
+3
-3
@@ -9,8 +9,8 @@ IDC_SCREENSHOT_PENCIL CURSOR "icons/lucide/pencil.cur"
|
||||
1 RT_MANIFEST "app.manifest"
|
||||
201 RCDATA "update.ps1"
|
||||
1 VERSIONINFO
|
||||
FILEVERSION 0,6,60,0
|
||||
PRODUCTVERSION 0,6,60,0
|
||||
FILEVERSION 0,6,62,0
|
||||
PRODUCTVERSION 0,6,62,0
|
||||
FILEOS VOS_NT_WINDOWS32
|
||||
FILETYPE VFT_APP
|
||||
BEGIN
|
||||
@@ -20,7 +20,7 @@ BEGIN
|
||||
BEGIN
|
||||
VALUE "FileDescription", "KSwitcher — native keyboard and clipboard utility"
|
||||
VALUE "ProductName", "KSwitcher"
|
||||
VALUE "FileVersion", "0.6.60"
|
||||
VALUE "FileVersion", "0.6.62"
|
||||
END
|
||||
END
|
||||
BLOCK "VarFileInfo"
|
||||
|
||||
+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,308 @@
|
||||
// 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{};
|
||||
HWND pickerOutline{};
|
||||
};
|
||||
constexpr UINT AppWindowPicked = WM_APP + 78;
|
||||
void showAppLanguageStatus(HWND w, const AppLanguagesView &v)
|
||||
{
|
||||
if (v.selection < 0)
|
||||
value(w, 721, L"Select an application, drag the crosshair, or enter an executable name.");
|
||||
else if (v.dirty || v.profiles.contains(v.applications[v.selection]))
|
||||
value(w, 721, L"Custom languages for this application. Other applications are unchanged.");
|
||||
else
|
||||
value(w, 721, L"Using default languages. Click languages to customize this application.");
|
||||
}
|
||||
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);
|
||||
showAppLanguageStatus(w, *v);
|
||||
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);
|
||||
}
|
||||
showAppLanguageStatus(w, *v);
|
||||
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;
|
||||
}
|
||||
void selectAppLanguageProfile(HWND w, AppLanguagesView *v, const std::wstring &app)
|
||||
{
|
||||
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())
|
||||
{
|
||||
auto row = SendDlgItemMessageW(w, 720, LB_ADDSTRING, 0, (LPARAM)app.c_str());
|
||||
if (row < 0) throw std::runtime_error("Could not add the application to the list.");
|
||||
v->applications.push_back(app);
|
||||
}
|
||||
v->selection = index;
|
||||
SendDlgItemMessageW(w, 720, LB_SETCURSEL, index, 0);
|
||||
fillAppLanguages(w, v);
|
||||
}
|
||||
HWND appPickerTarget(HWND button, LPARAM position)
|
||||
{
|
||||
POINT point{(short)LOWORD(position), (short)HIWORD(position)};
|
||||
ClientToScreen(button, &point);
|
||||
auto target = GetAncestor(WindowFromPoint(point), GA_ROOT);
|
||||
auto dialog = GetParent(button);
|
||||
return target && target != dialog && target != GetWindow(dialog, GW_OWNER) ? target : nullptr;
|
||||
}
|
||||
void updatePickerOutline(HWND dialog, AppLanguagesView &v, HWND target)
|
||||
{
|
||||
RECT bounds{};
|
||||
if (!target || !GetWindowRect(target, &bounds) || IsRectEmpty(&bounds))
|
||||
{
|
||||
if (v.pickerOutline) ShowWindow(v.pickerOutline, SW_HIDE);
|
||||
return;
|
||||
}
|
||||
if (!v.pickerOutline)
|
||||
{
|
||||
WNDCLASSW windowClass{};
|
||||
windowClass.lpfnWndProc = DefWindowProcW;
|
||||
windowClass.hInstance = GetModuleHandleW(nullptr);
|
||||
windowClass.hbrBackground = GetSysColorBrush(COLOR_HIGHLIGHT);
|
||||
windowClass.lpszClassName = L"KSwitcher.AppPickerOutline";
|
||||
RegisterClassW(&windowClass);
|
||||
v.pickerOutline = CreateWindowExW(WS_EX_LAYERED | WS_EX_TRANSPARENT | WS_EX_TOOLWINDOW | WS_EX_NOACTIVATE,
|
||||
windowClass.lpszClassName, nullptr, WS_POPUP, 0, 0, 0, 0, dialog, nullptr, windowClass.hInstance, nullptr);
|
||||
if (!v.pickerOutline) return;
|
||||
if (!SetLayeredWindowAttributes(v.pickerOutline, 0, 255, LWA_ALPHA))
|
||||
{
|
||||
DestroyWindow(v.pickerOutline);
|
||||
v.pickerOutline = nullptr;
|
||||
return;
|
||||
}
|
||||
}
|
||||
RECT previous{};
|
||||
if (IsWindowVisible(v.pickerOutline) && GetWindowRect(v.pickerOutline, &previous) && EqualRect(&bounds, &previous)) return;
|
||||
int width = bounds.right - bounds.left, height = bounds.bottom - bounds.top;
|
||||
int stroke = scale(target, 4);
|
||||
HRGN frame = CreateRectRgn(0, 0, width, height);
|
||||
if (!frame) return;
|
||||
if (width > 2 * stroke && height > 2 * stroke)
|
||||
{
|
||||
HRGN opening = CreateRectRgn(stroke, stroke, width - stroke, height - stroke);
|
||||
if (opening)
|
||||
{
|
||||
CombineRgn(frame, frame, opening, RGN_DIFF);
|
||||
DeleteObject(opening);
|
||||
}
|
||||
}
|
||||
if (!SetWindowRgn(v.pickerOutline, frame, TRUE))
|
||||
{
|
||||
DeleteObject(frame);
|
||||
return;
|
||||
}
|
||||
SetWindowPos(v.pickerOutline, HWND_TOPMOST, bounds.left, bounds.top, width, height, SWP_NOACTIVATE | SWP_SHOWWINDOW);
|
||||
}
|
||||
LRESULT CALLBACK appWindowPickerProc(HWND button, UINT msg, WPARAM wp, LPARAM lp, UINT_PTR id, DWORD_PTR)
|
||||
{
|
||||
HWND dialog = GetParent(button);
|
||||
auto *view = (AppLanguagesView *)GetWindowLongPtrW(dialog, GWLP_USERDATA);
|
||||
if (msg == WM_LBUTTONDOWN)
|
||||
{
|
||||
SetFocus(button);
|
||||
SetCapture(button);
|
||||
if (GetCapture() == button)
|
||||
{
|
||||
SetCursor(LoadCursorW(nullptr, IDC_CROSS));
|
||||
SetDlgItemTextW(dialog, 721, L"Drag to the application's window and release. Esc cancels.");
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
if (msg == WM_CAPTURECHANGED || msg == WM_NCDESTROY)
|
||||
{
|
||||
if (view && view->pickerOutline)
|
||||
{
|
||||
DestroyWindow(view->pickerOutline);
|
||||
view->pickerOutline = nullptr;
|
||||
}
|
||||
if (msg == WM_CAPTURECHANGED) SendMessageW(dialog, AppWindowPicked, 0, 0);
|
||||
}
|
||||
if (GetCapture() == button)
|
||||
{
|
||||
if (msg == WM_GETDLGCODE && wp == VK_ESCAPE) return DLGC_WANTMESSAGE;
|
||||
if (msg == WM_CANCELMODE || msg == WM_RBUTTONDOWN || (msg == WM_KEYDOWN && wp == VK_ESCAPE))
|
||||
{
|
||||
ReleaseCapture();
|
||||
return 0;
|
||||
}
|
||||
if (msg == WM_MOUSEMOVE || msg == WM_SETCURSOR)
|
||||
{
|
||||
SetCursor(LoadCursorW(nullptr, IDC_CROSS));
|
||||
if (msg == WM_MOUSEMOVE && view) updatePickerOutline(dialog, *view, appPickerTarget(button, lp));
|
||||
return TRUE;
|
||||
}
|
||||
if (msg == WM_LBUTTONUP)
|
||||
{
|
||||
HWND target = appPickerTarget(button, lp);
|
||||
ReleaseCapture();
|
||||
SendMessageW(dialog, AppWindowPicked, (WPARAM)target, 1);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
if (msg == WM_NCDESTROY) RemoveWindowSubclass(button, appWindowPickerProc, id);
|
||||
return DefSubclassProc(button, msg, wp, lp);
|
||||
}
|
||||
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 == AppWindowPicked)
|
||||
{
|
||||
if (!lp) { showAppLanguageStatus(w, *v); return 0; }
|
||||
HWND target = (HWND)wp;
|
||||
auto app = target && target != w && target != v->parent ? applicationName(processName(target)) : L"";
|
||||
if (app.empty()) value(w, 721, L"No application found. Release the crosshair over another window.");
|
||||
else selectAppLanguageProfile(w, v, app);
|
||||
return 0;
|
||||
}
|
||||
if (msg == WM_COMMAND)
|
||||
{
|
||||
int id = LOWORD(wp);
|
||||
if (id == 722 && HIWORD(wp) == LBN_SELCHANGE)
|
||||
{
|
||||
v->dirty = true;
|
||||
EnableWindow(GetDlgItem(w, 725), TRUE);
|
||||
showAppLanguageStatus(w, *v);
|
||||
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);
|
||||
selectAppLanguageProfile(w, v, app);
|
||||
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, 108);
|
||||
button(w, L"⊕ Drag to window", 728, 132, 10, 164);
|
||||
SetWindowSubclass(GetDlgItem(w, 728), appWindowPickerProc, 1, 0);
|
||||
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.62";
|
||||
inline constexpr UINT UpdateMessage = WM_APP + 7;
|
||||
inline constexpr UINT UpdateCheckIntervalMs = 6 * 60 * 60 * 1000;
|
||||
inline constexpr UINT UpdateNotificationRetryMs = 5 * 60 * 1000;
|
||||
|
||||
@@ -225,6 +225,7 @@ int main()
|
||||
ks::atomicWrite(std::filesystem::path(ks::executable()).parent_path() / L"chat-languages.png",
|
||||
ks::encodePng(ks::decodeImage(clip)));
|
||||
SendDlgItemMessageW(dialog, 711, LB_SETSEL, TRUE, -1);
|
||||
SendMessageW(dialog, WM_COMMAND, MAKEWPARAM(711, LBN_SELCHANGE), 0);
|
||||
SendMessageW(dialog, WM_COMMAND, IDCANCEL, 0);
|
||||
require(!IsWindow(dialog) && IsWindowEnabled(parent), "cancel closes dialog");
|
||||
require(!saveEnabled(),"cancelled chat edits do not enable Save");
|
||||
@@ -235,6 +236,7 @@ int main()
|
||||
SendMessageW(dialog, WM_COMMAND, MAKEWPARAM(710, LBN_SELCHANGE), 0);
|
||||
require(SendDlgItemMessageW(dialog, 711, LB_GETSELCOUNT, 0, 0) == 1, "cancel discards language edits");
|
||||
SendDlgItemMessageW(dialog, 711, LB_SETSEL, TRUE, -1);
|
||||
SendMessageW(dialog, WM_COMMAND, MAKEWPARAM(711, LBN_SELCHANGE), 0);
|
||||
SendMessageW(dialog, WM_COMMAND, IDOK, 0);
|
||||
require(!IsWindow(dialog) && IsWindowEnabled(parent), "profile applies to settings draft");
|
||||
require(saveEnabled() == (app.layouts.size()>1),"accepted chat edits refresh Save immediately");
|
||||
@@ -250,6 +252,8 @@ int main()
|
||||
SendDlgItemMessageW(dialog, 710, LB_SETCURSEL, index, 0);
|
||||
SendMessageW(dialog, WM_COMMAND, MAKEWPARAM(710, LBN_SELCHANGE), 0);
|
||||
SendMessageW(dialog, WM_COMMAND, 713, 0);
|
||||
require(IsWindow(dialog), "chat reset keeps dialog open");
|
||||
SendMessageW(dialog, WM_COMMAND, IDOK, 0);
|
||||
require(saveEnabled() && status().find(L"Unsaved changes")==0,"removing child profile immediately marks parent dirty");
|
||||
RedrawWindow(parent,nullptr,nullptr,RDW_INVALIDATE|RDW_UPDATENOW|RDW_ALLCHILDREN);
|
||||
GetClientRect(parent,&rect); dc=GetDC(parent);
|
||||
@@ -258,6 +262,97 @@ int main()
|
||||
ks::encodePng(ks::decodeImage(clip)));
|
||||
SendMessageW(parent, WM_COMMAND, IDOK, 0);
|
||||
require(!app.settings.chatCycles.contains(key), "remove profile restores default");
|
||||
// Existing shared sets become independent application drafts on edit.
|
||||
app.settings.appLanguageSets[L"Legacy shared"] = {{app.layouts[0].id}, {L"Codex.exe", L"notepad"}};
|
||||
ks::showSettings(app);
|
||||
parent = app.settingsWindow;
|
||||
SendMessageW(parent, WM_COMMAND, 263, 0);
|
||||
dialog = findTestWindow(L"KSwitcher.AppLanguages", nullptr);
|
||||
require(dialog && !IsWindowEnabled(parent), "application language editor opens");
|
||||
auto selectApp = [&](const wchar_t *name) {
|
||||
int index = (int)SendDlgItemMessageW(dialog, 720, LB_FINDSTRINGEXACT, -1, (LPARAM)name);
|
||||
require(index >= 0, "application listed");
|
||||
SendDlgItemMessageW(dialog, 720, LB_SETCURSEL, index, 0);
|
||||
SendMessageW(dialog, WM_COMMAND, MAKEWPARAM(720, LBN_SELCHANGE), 0);
|
||||
};
|
||||
selectApp(L"codex");
|
||||
require(SendDlgItemMessageW(dialog, 722, LB_GETSELCOUNT, 0, 0) == 1, "legacy languages displayed");
|
||||
SendDlgItemMessageW(dialog, 722, LB_SETSEL, TRUE, -1);
|
||||
SendMessageW(dialog, WM_COMMAND, MAKEWPARAM(722, LBN_SELCHANGE), 0);
|
||||
HWND picker = GetDlgItem(dialog, 728);
|
||||
RECT dialogBounds{};
|
||||
GetWindowRect(dialog, &dialogBounds);
|
||||
HWND target = CreateWindowExW(WS_EX_TOPMOST | WS_EX_NOACTIVATE, L"EDIT", L"Picker fixture",
|
||||
WS_POPUP | WS_VISIBLE, dialogBounds.left + 32, dialogBounds.top + 100, 120, 48,
|
||||
nullptr, nullptr, GetModuleHandleW(nullptr), nullptr);
|
||||
require(picker && target, "window picker and target available");
|
||||
POINT drop{dialogBounds.left + 33, dialogBounds.top + 101};
|
||||
require(GetAncestor(WindowFromPoint(drop), GA_ROOT) == target, "fixture window is under the drop point");
|
||||
ScreenToClient(picker, &drop);
|
||||
SendMessageW(picker, WM_LBUTTONDOWN, MK_LBUTTON, MAKELPARAM(8, 8));
|
||||
require(GetCapture() == picker, "window picker captures drag");
|
||||
SendMessageW(picker, WM_MOUSEMOVE, MK_LBUTTON, MAKELPARAM(drop.x, drop.y));
|
||||
HWND outline = findTestWindow(L"KSwitcher.AppPickerOutline", nullptr);
|
||||
RECT outlineBounds{}, targetBounds{};
|
||||
require(IsWindowVisible(outline) && GetWindowRect(outline, &outlineBounds) &&
|
||||
GetWindowRect(target, &targetBounds) && EqualRect(&outlineBounds, &targetBounds), "drag highlights the target window");
|
||||
SendMessageW(picker, WM_KEYDOWN, VK_ESCAPE, 0);
|
||||
require(!IsWindow(outline) && GetCapture() != picker, "Escape clears the outline and releases capture");
|
||||
SendMessageW(picker, WM_LBUTTONDOWN, MK_LBUTTON, MAKELPARAM(8, 8));
|
||||
SendMessageW(picker, WM_MOUSEMOVE, MK_LBUTTON, MAKELPARAM(drop.x, drop.y));
|
||||
outline = findTestWindow(L"KSwitcher.AppPickerOutline", nullptr);
|
||||
SendMessageW(picker, WM_LBUTTONUP, 0, MAKELPARAM(drop.x, drop.y));
|
||||
require(!IsWindow(outline), "dropping clears the outline");
|
||||
auto pickedApp = ks::applicationName(ks::processName(target));
|
||||
DestroyWindow(target);
|
||||
auto pickedRow = SendDlgItemMessageW(dialog, 720, LB_FINDSTRINGEXACT, -1, (LPARAM)pickedApp.c_str());
|
||||
require(GetCapture() != picker && pickedRow >= 0 &&
|
||||
SendDlgItemMessageW(dialog, 720, LB_GETCURSEL, 0, 0) == pickedRow, "dropping on a window selects its executable");
|
||||
selectApp(L"notepad");
|
||||
require(SendDlgItemMessageW(dialog, 722, LB_GETSELCOUNT, 0, 0) == 1, "editing one app leaves formerly shared app unchanged");
|
||||
selectApp(L"codex");
|
||||
require(SendDlgItemMessageW(dialog, 722, LB_GETSELCOUNT, 0, 0) == (LRESULT)app.layouts.size(), "application navigation preserves drafts");
|
||||
RedrawWindow(dialog, nullptr, nullptr, RDW_INVALIDATE | RDW_UPDATENOW | RDW_ALLCHILDREN);
|
||||
GetClientRect(dialog, &rect);
|
||||
dc = GetDC(dialog);
|
||||
clip = ks::screenshotClip(dc, rect.right, rect.bottom, {});
|
||||
ReleaseDC(dialog, dc);
|
||||
ks::atomicWrite(std::filesystem::path(ks::executable()).parent_path() / L"application-languages.png",
|
||||
ks::encodePng(ks::decodeImage(clip)));
|
||||
SendMessageW(dialog, WM_COMMAND, IDCANCEL, 0);
|
||||
SendMessageW(parent, WM_COMMAND, IDOK, 0);
|
||||
require(app.settings.appLanguageSets.size() == 1 && app.settings.appLanguageSets.contains(L"Legacy shared"), "cancel preserves legacy settings unchanged");
|
||||
ks::showSettings(app);
|
||||
parent = app.settingsWindow;
|
||||
SendMessageW(parent, WM_COMMAND, 263, 0);
|
||||
dialog = findTestWindow(L"KSwitcher.AppLanguages", nullptr);
|
||||
selectApp(L"codex");
|
||||
SendDlgItemMessageW(dialog, 722, LB_SETSEL, TRUE, -1);
|
||||
SendMessageW(dialog, WM_COMMAND, MAKEWPARAM(722, LBN_SELCHANGE), 0);
|
||||
selectApp(L"notepad");
|
||||
SetDlgItemTextW(dialog, 726, L"Fixture-App.EXE");
|
||||
SendMessageW(dialog, WM_COMMAND, 727, 0);
|
||||
require(!IsWindowEnabled(GetDlgItem(dialog, 725)), "new application inherits defaults");
|
||||
selectApp(L"codex");
|
||||
SendMessageW(dialog, WM_COMMAND, IDOK, 0);
|
||||
SendMessageW(parent, WM_COMMAND, IDOK, 0);
|
||||
require(app.settings.appLanguageSets.size() == 2, "viewing an app does not create a profile");
|
||||
require(app.settings.appLanguageSets.at(L"codex").languages.size() == app.layouts.size(), "edited application languages saved");
|
||||
require(app.settings.appLanguageSets.at(L"notepad").languages == std::vector<std::wstring>{app.layouts[0].id}, "other application retains original languages");
|
||||
require(ks::loadSettings(root / L"settings.json", app.layouts).appLanguageSets == app.settings.appLanguageSets, "independent application profiles saved to disk");
|
||||
ks::showSettings(app);
|
||||
parent = app.settingsWindow;
|
||||
SendMessageW(parent, WM_COMMAND, 263, 0);
|
||||
dialog = findTestWindow(L"KSwitcher.AppLanguages", nullptr);
|
||||
selectApp(L"codex");
|
||||
SendMessageW(dialog, WM_COMMAND, 725, 0);
|
||||
selectApp(L"notepad");
|
||||
selectApp(L"codex");
|
||||
require(!IsWindowEnabled(GetDlgItem(dialog, 725)), "default reset survives navigation");
|
||||
SendMessageW(dialog, WM_COMMAND, IDOK, 0);
|
||||
SendMessageW(parent, WM_COMMAND, IDOK, 0);
|
||||
require(!ks::applicationLanguageSet(app.settings, L"codex"), "use defaults removes only selected application profile");
|
||||
require(ks::applicationLanguageSet(app.settings, L"notepad") != nullptr, "default reset leaves other application profile intact");
|
||||
app.settings.snippets = {{L";scope", L"Example", L"scope-fixture"}};
|
||||
ks::showSettings(app); parent=app.settingsWindow;
|
||||
require(!saveEnabled(),"Settings starts clean before separately saved snippet edit");
|
||||
|
||||
Reference in new issue
Block a user