Add window-based target picker as the default; process list goes advanced
New host/src/inject/window_list.{hpp,cpp} enumerates visible, titled, non-tool
top-level (alt-tab-style) windows via EnumWindows -- root-owner only, our own
process excluded -- and maps each to its owning pid + image name. The Injection
panel now defaults to this window list (each row "title [process.exe pid]", with a
filter over title or process), since there are far fewer windows than processes and
a window maps straight to the HWND the capturer wants. The full process list stays
as the advanced picker under Debug details.
Verified live: launched the host and captured its window -- the picker lists real
windows (Discord/Firefox/Explorer/...) in "title [exe pid]" form, filter present,
and the host's own window correctly excluded. x64 build green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
10
README.md
10
README.md
@@ -75,16 +75,6 @@ is removed from this list once done — so the top item is always next. The
|
|||||||
self-verifiable tooling / UI / input items come first; the game-pipeline items that
|
self-verifiable tooling / UI / input items come first; the game-pipeline items that
|
||||||
need a real game (and Remote Play) to fully validate come last.
|
need a real game (and Remote Play) to fully validate come last.
|
||||||
|
|
||||||
- **Select targets by window, not just process.** A flat process list is fine as an
|
|
||||||
advanced/debug view, but the default should be a **window list** — there are far
|
|
||||||
fewer top-level windows than processes, and a window directly yields the HWND the
|
|
||||||
WGC capturer and focus spoof already want. *How:* add a window enumerator
|
|
||||||
(`EnumWindows`, keeping visible, titled, non-tool top-level windows —
|
|
||||||
`IsWindowVisible`, `GetWindowTextLength > 0`, exclude `WS_EX_TOOLWINDOW` and our own
|
|
||||||
HWND, resolve to the root owner) and map each via `GetWindowThreadProcessId` → pid →
|
|
||||||
image name. Show **title + process name + pid** with a filter box like the process
|
|
||||||
list; injecting by window injects into its pid and hands the HWND straight to
|
|
||||||
capture. Keep the process list behind "Debug details" as the advanced path.
|
|
||||||
- **Auto-size and lay out the overlay windows so none need manual resizing.** Panels
|
- **Auto-size and lay out the overlay windows so none need manual resizing.** Panels
|
||||||
currently `Begin` at default cascade positions, so they overlap and clip. Give each
|
currently `Begin` at default cascade positions, so they overlap and clip. Give each
|
||||||
`ImGuiWindowFlags_AlwaysAutoResize` and an initial position computed from
|
`ImGuiWindowFlags_AlwaysAutoResize` and an initial position computed from
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ add_executable(coop_host WIN32
|
|||||||
src/ui/app_chrome.cpp
|
src/ui/app_chrome.cpp
|
||||||
src/input/xinput_source.cpp
|
src/input/xinput_source.cpp
|
||||||
src/inject/process_list.cpp
|
src/inject/process_list.cpp
|
||||||
|
src/inject/window_list.cpp
|
||||||
src/inject/injector.cpp
|
src/inject/injector.cpp
|
||||||
src/ipc/ipc_server.cpp
|
src/ipc/ipc_server.cpp
|
||||||
src/capture/frame_renderer.cpp
|
src/capture/frame_renderer.cpp
|
||||||
|
|||||||
82
host/src/inject/window_list.cpp
Normal file
82
host/src/inject/window_list.cpp
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
#include "inject/window_list.hpp"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <unordered_map>
|
||||||
|
|
||||||
|
#include <windows.h>
|
||||||
|
|
||||||
|
#include "inject/process_list.hpp"
|
||||||
|
|
||||||
|
namespace coop
|
||||||
|
{
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
|
||||||
|
struct EnumCtx
|
||||||
|
{
|
||||||
|
std::vector<WindowEntry>* out;
|
||||||
|
const std::unordered_map<unsigned long, std::wstring>* names;
|
||||||
|
DWORD self_pid;
|
||||||
|
};
|
||||||
|
|
||||||
|
BOOL CALLBACK enum_proc(HWND hwnd, LPARAM lparam)
|
||||||
|
{
|
||||||
|
auto* ctx = reinterpret_cast<EnumCtx*>(lparam);
|
||||||
|
|
||||||
|
// Keep only "alt-tab" windows: visible, titled, root-owner, non-tool, not ours.
|
||||||
|
if (!IsWindowVisible(hwnd) || GetAncestor(hwnd, GA_ROOTOWNER) != hwnd)
|
||||||
|
{
|
||||||
|
return TRUE;
|
||||||
|
}
|
||||||
|
const int len = GetWindowTextLengthW(hwnd);
|
||||||
|
if (len <= 0)
|
||||||
|
{
|
||||||
|
return TRUE;
|
||||||
|
}
|
||||||
|
if ((GetWindowLongW(hwnd, GWL_EXSTYLE) & WS_EX_TOOLWINDOW) != 0)
|
||||||
|
{
|
||||||
|
return TRUE;
|
||||||
|
}
|
||||||
|
DWORD pid = 0;
|
||||||
|
GetWindowThreadProcessId(hwnd, &pid);
|
||||||
|
if (pid == 0 || pid == ctx->self_pid)
|
||||||
|
{
|
||||||
|
return TRUE;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::wstring title(static_cast<std::size_t>(len), L'\0');
|
||||||
|
GetWindowTextW(hwnd, title.data(), len + 1);
|
||||||
|
|
||||||
|
std::wstring exe;
|
||||||
|
if (const auto it = ctx->names->find(pid); it != ctx->names->end())
|
||||||
|
{
|
||||||
|
exe = it->second;
|
||||||
|
}
|
||||||
|
ctx->out->push_back(WindowEntry{pid, hwnd, std::move(title), std::move(exe)});
|
||||||
|
return TRUE;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
std::vector<WindowEntry> list_windows()
|
||||||
|
{
|
||||||
|
// pid -> image name, so each window can show its owning process without a separate
|
||||||
|
// OpenProcess per window.
|
||||||
|
std::unordered_map<unsigned long, std::wstring> names;
|
||||||
|
for (const ProcessEntry& p : list_processes())
|
||||||
|
{
|
||||||
|
names.emplace(p.pid, p.exe_name);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<WindowEntry> out;
|
||||||
|
EnumCtx ctx{&out, &names, GetCurrentProcessId()};
|
||||||
|
EnumWindows(&enum_proc, reinterpret_cast<LPARAM>(&ctx));
|
||||||
|
|
||||||
|
std::sort(out.begin(), out.end(), [](const WindowEntry& a, const WindowEntry& b) {
|
||||||
|
return _wcsicmp(a.title.c_str(), b.title.c_str()) < 0;
|
||||||
|
});
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace coop
|
||||||
24
host/src/inject/window_list.hpp
Normal file
24
host/src/inject/window_list.hpp
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
// Enumerates visible top-level windows for the injection target picker. There are
|
||||||
|
// far fewer windows than processes, and a window maps directly to the HWND the
|
||||||
|
// capturer wants, so this is the default (friendlier) way to pick a game.
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace coop
|
||||||
|
{
|
||||||
|
|
||||||
|
struct WindowEntry
|
||||||
|
{
|
||||||
|
unsigned long pid = 0; // owning process id
|
||||||
|
void* hwnd = nullptr; // HWND (opaque here to keep windows.h out of the header)
|
||||||
|
std::wstring title; // window caption
|
||||||
|
std::wstring exe_name; // owning process image base name, e.g. "game.exe"
|
||||||
|
};
|
||||||
|
|
||||||
|
// Snapshot of the visible, titled, non-tool top-level (alt-tab-style) windows, with
|
||||||
|
// our own process's windows excluded. Sorted by title (case-insensitive).
|
||||||
|
std::vector<WindowEntry> list_windows();
|
||||||
|
|
||||||
|
} // namespace coop
|
||||||
@@ -84,7 +84,13 @@ std::wstring hook_dll_path()
|
|||||||
|
|
||||||
InjectionPanel::InjectionPanel()
|
InjectionPanel::InjectionPanel()
|
||||||
{
|
{
|
||||||
refresh_processes();
|
refresh_targets();
|
||||||
|
}
|
||||||
|
|
||||||
|
void InjectionPanel::refresh_targets()
|
||||||
|
{
|
||||||
|
windows_ = list_windows();
|
||||||
|
processes_ = list_processes();
|
||||||
}
|
}
|
||||||
|
|
||||||
InjectionPanel::~InjectionPanel()
|
InjectionPanel::~InjectionPanel()
|
||||||
@@ -507,16 +513,44 @@ void InjectionPanel::draw(bool debug_details)
|
|||||||
ImGui::Separator();
|
ImGui::Separator();
|
||||||
}
|
}
|
||||||
|
|
||||||
ImGui::TextUnformatted("Target process");
|
ImGui::TextUnformatted("Target window");
|
||||||
if (ImGui::Button("Refresh"))
|
if (ImGui::Button("Refresh"))
|
||||||
{
|
{
|
||||||
refresh_processes();
|
refresh_targets();
|
||||||
}
|
}
|
||||||
ImGui::SameLine();
|
ImGui::SameLine();
|
||||||
ImGui::SetNextItemWidth(-1.0f);
|
ImGui::SetNextItemWidth(-1.0f);
|
||||||
ImGui::InputTextWithHint("##filter", "filter by name...", filter_, sizeof(filter_));
|
ImGui::InputTextWithHint("##wfilter", "filter by title or process...", window_filter_, sizeof(window_filter_));
|
||||||
|
|
||||||
if (ImGui::BeginListBox("##processes", ImVec2(-1.0f, 200.0f)))
|
if (ImGui::BeginListBox("##windows", ImVec2(-1.0f, 180.0f)))
|
||||||
|
{
|
||||||
|
for (const WindowEntry& w : windows_)
|
||||||
|
{
|
||||||
|
if (!contains_ci(w.title, window_filter_) && !contains_ci(w.exe_name, window_filter_))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const bool selected = w.pid == selected_pid_;
|
||||||
|
char label[400];
|
||||||
|
snprintf(label, sizeof(label), "%-32s [%s %lu]", narrow(w.title).c_str(),
|
||||||
|
narrow(w.exe_name).c_str(), w.pid);
|
||||||
|
if (ImGui::Selectable(label, selected))
|
||||||
|
{
|
||||||
|
selected_pid_ = w.pid;
|
||||||
|
selected_name_ = w.exe_name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ImGui::EndListBox();
|
||||||
|
}
|
||||||
|
|
||||||
|
// The full process list is the advanced fallback (e.g. a windowless game host),
|
||||||
|
// kept out of the way unless the operator wants it.
|
||||||
|
if (debug_details)
|
||||||
|
{
|
||||||
|
ImGui::SeparatorText("All processes (advanced)");
|
||||||
|
ImGui::SetNextItemWidth(-1.0f);
|
||||||
|
ImGui::InputTextWithHint("##filter", "filter by name...", filter_, sizeof(filter_));
|
||||||
|
if (ImGui::BeginListBox("##processes", ImVec2(-1.0f, 160.0f)))
|
||||||
{
|
{
|
||||||
for (const ProcessEntry& entry : processes_)
|
for (const ProcessEntry& entry : processes_)
|
||||||
{
|
{
|
||||||
@@ -535,6 +569,7 @@ void InjectionPanel::draw(bool debug_details)
|
|||||||
}
|
}
|
||||||
ImGui::EndListBox();
|
ImGui::EndListBox();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const bool can_inject = selected_pid_ != 0;
|
const bool can_inject = selected_pid_ != 0;
|
||||||
ImGui::BeginDisabled(!can_inject);
|
ImGui::BeginDisabled(!can_inject);
|
||||||
|
|||||||
@@ -11,6 +11,7 @@
|
|||||||
#include "coop/protocol.hpp"
|
#include "coop/protocol.hpp"
|
||||||
#include "imgui.h"
|
#include "imgui.h"
|
||||||
#include "inject/process_list.hpp"
|
#include "inject/process_list.hpp"
|
||||||
|
#include "inject/window_list.hpp"
|
||||||
#include "ipc/ipc_server.hpp"
|
#include "ipc/ipc_server.hpp"
|
||||||
|
|
||||||
namespace coop
|
namespace coop
|
||||||
@@ -106,6 +107,7 @@ public:
|
|||||||
}
|
}
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
void refresh_targets(); // refresh both the window list and the process list
|
||||||
void refresh_processes();
|
void refresh_processes();
|
||||||
void inject_selected();
|
void inject_selected();
|
||||||
void reattach(); // re-inject a relaunched same-name target (Terminated state)
|
void reattach(); // re-inject a relaunched same-name target (Terminated state)
|
||||||
@@ -115,7 +117,9 @@ private:
|
|||||||
void draw_hook_list(const HookStatusView& status);
|
void draw_hook_list(const HookStatusView& status);
|
||||||
void draw_hook_status(bool debug_details);
|
void draw_hook_status(bool debug_details);
|
||||||
|
|
||||||
std::vector<ProcessEntry> processes_;
|
std::vector<WindowEntry> windows_; // default picker (visible top-level windows)
|
||||||
|
std::vector<ProcessEntry> processes_; // advanced picker (all processes)
|
||||||
|
char window_filter_[128] = {};
|
||||||
char filter_[128] = {};
|
char filter_[128] = {};
|
||||||
unsigned long selected_pid_ = 0;
|
unsigned long selected_pid_ = 0;
|
||||||
std::wstring selected_name_;
|
std::wstring selected_name_;
|
||||||
|
|||||||
Reference in New Issue
Block a user