Pick an application by dragging onto its highlighted window
This commit is contained in:
1 parent
5d96abb83e
commit
a0e1de40ef
3 files changed
+181
-16
No files matched your search
@@ -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
|
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
|
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.
|
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
|
Applications without a custom profile use the default languages; merely viewing
|
||||||
an application does not create a profile. **Use defaults** clears only the selected
|
an application does not create a profile. **Use defaults** clears only the selected
|
||||||
application's custom choice. Switching applications preserves pending edits.
|
application's custom choice. Switching applications preserves pending edits.
|
||||||
@@ -937,6 +939,9 @@ settings export/import.
|
|||||||
цього вікна. У списку є відкриті програми та збережені профілі; закриту програму
|
цього вікна. У списку є відкриті програми та збережені профілі; закриту програму
|
||||||
можна додати за назвою файла, наприклад `Codex.exe`. Регістр і `.exe` не мають
|
можна додати за назвою файла, наприклад `Codex.exe`. Регістр і `.exe` не мають
|
||||||
значення. Правило охоплює всі вікна програми, зокрема всі вкладки браузера.
|
значення. Правило охоплює всі вікна програми, зокрема всі вкладки браузера.
|
||||||
|
Щоб визначити відкриту програму, перетягни приціл **Drag to window** на її вікно.
|
||||||
|
Рамка показує, яке вікно буде вибрано. Відпусти приціл, щоб вибрати програму,
|
||||||
|
або натисни **Esc**, щоб скасувати.
|
||||||
Без окремого профілю діють загальні мови; простий перегляд програми не створює
|
Без окремого профілю діють загальні мови; простий перегляд програми не створює
|
||||||
профіль. **Use defaults** повертає лише вибрану програму до загальних мов.
|
профіль. **Use defaults** повертає лише вибрану програму до загальних мов.
|
||||||
Перехід між програмами зберігає чернетки. **Use languages**, потім **Save**
|
Перехід між програмами зберігає чернетки. **Use languages**, потім **Save**
|
||||||
|
|||||||
+147
-16
@@ -12,7 +12,18 @@ struct AppLanguagesView
|
|||||||
std::vector<std::wstring> applications;
|
std::vector<std::wstring> applications;
|
||||||
int selection = -1;
|
int selection = -1;
|
||||||
bool dirty{};
|
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)
|
void fillAppLanguages(HWND w, AppLanguagesView *v)
|
||||||
{
|
{
|
||||||
bool selected = v->selection >= 0 && v->selection < (int)v->applications.size();
|
bool selected = v->selection >= 0 && v->selection < (int)v->applications.size();
|
||||||
@@ -21,7 +32,7 @@ void fillAppLanguages(HWND w, AppLanguagesView *v)
|
|||||||
if (!selected)
|
if (!selected)
|
||||||
{
|
{
|
||||||
EnableWindow(GetDlgItem(w, 725), FALSE);
|
EnableWindow(GetDlgItem(w, 725), FALSE);
|
||||||
value(w, 721, L"Select an application, or add its executable name below.");
|
showAppLanguageStatus(w, *v);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
auto found = v->profiles.find(v->applications[v->selection]);
|
auto found = v->profiles.find(v->applications[v->selection]);
|
||||||
@@ -32,8 +43,7 @@ void fillAppLanguages(HWND w, AppLanguagesView *v)
|
|||||||
SendDlgItemMessageW(w, 722, LB_SETSEL,
|
SendDlgItemMessageW(w, 722, LB_SETSEL,
|
||||||
std::find(cycle.begin(), cycle.end(), id) != cycle.end(), i);
|
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." :
|
showAppLanguageStatus(w, *v);
|
||||||
L"Custom languages for this application. Other applications are unchanged.");
|
|
||||||
EnableWindow(GetDlgItem(w, 725), found != v->profiles.end());
|
EnableWindow(GetDlgItem(w, 725), found != v->profiles.end());
|
||||||
}
|
}
|
||||||
void collectAppLanguages(HWND w, AppLanguagesView *v)
|
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->profiles[v->applications[v->selection]] = std::move(cycle);
|
||||||
v->dirty = false;
|
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)
|
LRESULT CALLBACK appLanguagesProc(HWND w, UINT msg, WPARAM wp, LPARAM lp)
|
||||||
{
|
{
|
||||||
auto *v = (AppLanguagesView *)GetWindowLongPtrW(w, GWLP_USERDATA);
|
auto *v = (AppLanguagesView *)GetWindowLongPtrW(w, GWLP_USERDATA);
|
||||||
if (!v) return DefWindowProcW(w, msg, wp, lp);
|
if (!v) return DefWindowProcW(w, msg, wp, lp);
|
||||||
try
|
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)
|
if (msg == WM_COMMAND)
|
||||||
{
|
{
|
||||||
int id = LOWORD(wp);
|
int id = LOWORD(wp);
|
||||||
@@ -60,7 +199,7 @@ LRESULT CALLBACK appLanguagesProc(HWND w, UINT msg, WPARAM wp, LPARAM lp)
|
|||||||
{
|
{
|
||||||
v->dirty = true;
|
v->dirty = true;
|
||||||
EnableWindow(GetDlgItem(w, 725), TRUE);
|
EnableWindow(GetDlgItem(w, 725), TRUE);
|
||||||
value(w, 721, L"Custom languages for this application. Other applications are unchanged.");
|
showAppLanguageStatus(w, *v);
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
if (id == 720 && HIWORD(wp) == LBN_SELCHANGE)
|
if (id == 720 && HIWORD(wp) == LBN_SELCHANGE)
|
||||||
@@ -85,17 +224,7 @@ LRESULT CALLBACK appLanguagesProc(HWND w, UINT msg, WPARAM wp, LPARAM lp)
|
|||||||
Settings check;
|
Settings check;
|
||||||
check.appLanguageSets[L"check"] = {{L"win:04090409"}, {app}};
|
check.appLanguageSets[L"check"] = {{L"win:04090409"}, {app}};
|
||||||
validateSettings(check);
|
validateSettings(check);
|
||||||
collectAppLanguages(w, v);
|
selectAppLanguageProfile(w, v, app);
|
||||||
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"");
|
value(w, 726, L"");
|
||||||
SetFocus(GetDlgItem(w, 722));
|
SetFocus(GetDlgItem(w, 722));
|
||||||
return 0;
|
return 0;
|
||||||
@@ -148,7 +277,9 @@ void showAppLanguages(HWND parent, SettingsView *settings)
|
|||||||
v->applications.assign(apps.begin(), apps.end());
|
v->applications.assign(apps.begin(), apps.end());
|
||||||
SetWindowLongPtrW(w, GWLP_HWNDPARENT, (LONG_PTR)parent);
|
SetWindowLongPtrW(w, GWLP_HWNDPARENT, (LONG_PTR)parent);
|
||||||
SetWindowLongPtrW(w, GWLP_USERDATA, (LONG_PTR)v);
|
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,
|
control(w, L"LISTBOX", L"", WS_TABSTOP | WS_VSCROLL | LBS_NOTIFY | LBS_NOINTEGRALHEIGHT | WS_BORDER,
|
||||||
720, 18, 44, 278, 280);
|
720, 18, 44, 278, 280);
|
||||||
label(w, L"Allowed languages (click to include)", 316, 16, 386);
|
label(w, L"Allowed languages (click to include)", 316, 16, 386);
|
||||||
|
|||||||
@@ -279,6 +279,35 @@ int main()
|
|||||||
require(SendDlgItemMessageW(dialog, 722, LB_GETSELCOUNT, 0, 0) == 1, "legacy languages displayed");
|
require(SendDlgItemMessageW(dialog, 722, LB_GETSELCOUNT, 0, 0) == 1, "legacy languages displayed");
|
||||||
SendDlgItemMessageW(dialog, 722, LB_SETSEL, TRUE, -1);
|
SendDlgItemMessageW(dialog, 722, LB_SETSEL, TRUE, -1);
|
||||||
SendMessageW(dialog, WM_COMMAND, MAKEWPARAM(722, LBN_SELCHANGE), 0);
|
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");
|
selectApp(L"notepad");
|
||||||
require(SendDlgItemMessageW(dialog, 722, LB_GETSELCOUNT, 0, 0) == 1, "editing one app leaves formerly shared app unchanged");
|
require(SendDlgItemMessageW(dialog, 722, LB_GETSELCOUNT, 0, 0) == 1, "editing one app leaves formerly shared app unchanged");
|
||||||
selectApp(L"codex");
|
selectApp(L"codex");
|
||||||
|
|||||||
Reference in new issue
Block a user