Files

120 lines
4.2 KiB
C++

// SPDX-License-Identifier: LGPL-2.1-or-later
#include "list_view.h"
#include "control_class.h"
#include "native_query.h"
#include <commctrl.h>
#include <cstdint>
#include <cstddef>
#include <cstring>
#include <iterator>
namespace {
using native_control::query;
// LVM_GETITEMTEXT is not marshalled by USER32. The structure and text buffer
// must live in the target process, with pointers matching its architecture.
struct Item32 {
uint32_t mask;
int32_t item, subItem;
uint32_t state, stateMask, text;
int32_t capacity;
};
struct Item64 {
uint32_t mask;
int32_t item, subItem;
uint32_t state, stateMask, padding;
uint64_t text;
int32_t capacity;
uint32_t padding2;
};
static_assert(offsetof(Item32, text) == 20);
static_assert(offsetof(Item64, text) == 24);
constexpr size_t textOffset = 128;
constexpr size_t textCapacity = 1024;
constexpr size_t bufferSize = textOffset + textCapacity * sizeof(wchar_t);
}
bool is_list_view(HWND window) {
wchar_t className[64] = {};
return GetClassNameW(window, className, std::size(className)) &&
wine_access_class_matches(className, L"SysListView32");
}
int list_view_index(HWND window, int *count) {
DWORD_PTR result = 0;
*count = 0;
if (!query(window, LVM_GETITEMCOUNT, 0, 0, &result) ||
static_cast<LONG_PTR>(result) <= 0)
return -1;
*count = static_cast<int>(result);
if (query(window, LVM_GETNEXTITEM, static_cast<WPARAM>(-1), LVNI_FOCUSED,
&result) && static_cast<LONG_PTR>(result) >= 0)
return static_cast<int>(result);
if (query(window, LVM_GETNEXTITEM, static_cast<WPARAM>(-1), LVNI_SELECTED,
&result) && static_cast<LONG_PTR>(result) >= 0)
return static_cast<int>(result);
// An installer can focus a populated list without selecting any row.
return 0;
}
ListViewRow read_list_view(HWND window) {
ListViewRow row;
row.index = list_view_index(window, &row.count);
if (row.index < 0 || row.index >= row.count)
return row;
DWORD_PTR state = 0;
if (query(window, LVM_GETITEMSTATE, row.index, LVIS_SELECTED, &state))
row.selected = state & LVIS_SELECTED;
row.checked = list_view_checked(window, row.index);
native_control::RemoteBuffer remote(window, bufferSize);
if (!remote) return row;
const bool wow64 = remote.is32Bit();
DWORD_PTR header = 0, columnCount = 1;
if (query(window, LVM_GETHEADER, 0, 0, &header) && header != 0) {
DWORD_PTR columns = 0;
if (query(reinterpret_cast<HWND>(header), HDM_GETITEMCOUNT, 0, 0, &columns) &&
columns > 0) columnCount = columns;
}
// Keep provider work and the protocol payload bounded even for huge tables.
if (columnCount > 8) columnCount = 8;
for (DWORD_PTR column = 0; column < columnCount; ++column) {
BYTE buffer[bufferSize] = {};
const auto textAddress = remote.address(textOffset);
if (wow64) {
Item32 item{LVIF_TEXT, row.index, static_cast<int32_t>(column),
0, 0, static_cast<uint32_t>(textAddress), textCapacity};
memcpy(buffer, &item, sizeof(item));
} else {
Item64 item{LVIF_TEXT, row.index, static_cast<int32_t>(column),
0, 0, 0, textAddress, textCapacity, 0};
memcpy(buffer, &item, sizeof(item));
}
DWORD_PTR result = 0;
wchar_t text[textCapacity] = {};
if (!remote.write(buffer, sizeof(buffer)) ||
!query(window, LVM_GETITEMTEXTW, row.index, static_cast<LPARAM>(remote.address()), &result) ||
!remote.read(text, sizeof(text), textOffset))
break;
text[textCapacity - 1] = L'\0';
if (!text[0]) continue;
char utf8[textCapacity * 4] = {};
if (!WideCharToMultiByte(CP_UTF8, 0, text, -1, utf8, sizeof(utf8), nullptr, nullptr))
continue;
if (!row.text.empty()) row.text += ", ";
row.text += utf8;
}
return row;
}
int list_view_checked(HWND window, int index) {
DWORD_PTR style = 0, state = 0;
if (index < 0 ||
!query(window, LVM_GETEXTENDEDLISTVIEWSTYLE, 0, 0, &style) ||
!(style & LVS_EX_CHECKBOXES) ||
!query(window, LVM_GETITEMSTATE, index, LVIS_STATEIMAGEMASK, &state))
return -1;
const DWORD_PTR image = (state & LVIS_STATEIMAGEMASK) >> 12;
// Other state images are application-defined; do not invent a check state.
return image == 1 ? 0 : image == 2 ? 1 : -1;
}