Pick an application by dragging onto its highlighted window

This commit is contained in:
agrynco committed 2026-11-10 00:43:02 +02:00
1 parent 5d96abb83e
commit a0e1de40ef
3 files changed
+181 -16

No files matched your search

+5
View File
@@ -913,6 +913,8 @@ 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.
@@ -937,6 +939,9 @@ settings export/import.
цього вікна. У списку є відкриті програми та збережені профілі; закриту програму
можна додати за назвою файла, наприклад `Codex.exe`. Регістр і `.exe` не мають
значення. Правило охоплює всі вікна програми, зокрема всі вкладки браузера.
Щоб визначити відкриту програму, перетягни приціл **Drag to window** на її вікно.
Рамка показує, яке вікно буде вибрано. Відпусти приціл, щоб вибрати програму,
або натисни **Esc**, щоб скасувати.
Без окремого профілю діють загальні мови; простий перегляд програми не створює
профіль. **Use defaults** повертає лише вибрану програму до загальних мов.
Перехід між програмами зберігає чернетки. **Use languages**, потім **Save**
+147 -16
View File
@@ -12,7 +12,18 @@ struct AppLanguagesView
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();
@@ -21,7 +32,7 @@ void fillAppLanguages(HWND w, AppLanguagesView *v)
if (!selected)
{
EnableWindow(GetDlgItem(w, 725), FALSE);
value(w, 721, L"Select an application, or add its executable name below.");
showAppLanguageStatus(w, *v);
return;
}
auto found = v->profiles.find(v->applications[v->selection]);
@@ -32,8 +43,7 @@ void fillAppLanguages(HWND w, AppLanguagesView *v)
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.");
showAppLanguageStatus(w, *v);
EnableWindow(GetDlgItem(w, 725), found != v->profiles.end());
}
void collectAppLanguages(HWND w, AppLanguagesView *v)
@@ -47,12 +57,141 @@ void collectAppLanguages(HWND w, AppLanguagesView *v)
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);
@@ -60,7 +199,7 @@ LRESULT CALLBACK appLanguagesProc(HWND w, UINT msg, WPARAM wp, LPARAM lp)
{
v->dirty = true;
EnableWindow(GetDlgItem(w, 725), TRUE);
value(w, 721, L"Custom languages for this application. Other applications are unchanged.");
showAppLanguageStatus(w, *v);
return 0;
}
if (id == 720 && HIWORD(wp) == LBN_SELCHANGE)
@@ -85,17 +224,7 @@ LRESULT CALLBACK appLanguagesProc(HWND w, UINT msg, WPARAM wp, LPARAM lp)
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);
selectAppLanguageProfile(w, v, app);
value(w, 726, L"");
SetFocus(GetDlgItem(w, 722));
return 0;
@@ -148,7 +277,9 @@ void showAppLanguages(HWND parent, SettingsView *settings)
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);
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);
+29
View File
@@ -279,6 +279,35 @@ int main()
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");