diff foosdk/sdk/libPPUI/CListControl-Subst.cpp @ 1:20d02a178406 default tip

*: check in everything else yay
author Paper <paper@tflc.us>
date Mon, 05 Jan 2026 02:15:46 -0500
parents
children
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/foosdk/sdk/libPPUI/CListControl-Subst.cpp	Mon Jan 05 02:15:46 2026 -0500
@@ -0,0 +1,1503 @@
+#include "stdafx.h"
+#include "CListControlComplete.h"
+#include "CListControl-Subst.h"
+#include "CWindowCreateAndDelete.h"
+#include <pfc/string-conv-lite.h>
+#include "ImplementOnFinalMessage.h"
+#include "CListControl-Cells.h"
+#include "windowLifetime.h"
+
+#define I_IMAGEREALLYNONE (-3)
+
+namespace {
+
+	static int safeFillText(wchar_t* psz, int cch, pfc::wstringLite const& text) {
+		int len = (int)text.length();
+		int lim = cch - 1;
+		if (len > lim) len = lim;
+		for (int i = 0; i < len; ++i) psz[i] = text[i];
+		psz[len] = 0;
+		return len;
+	}
+
+	class CListControl_ListViewBase : public CListControlComplete {
+	protected:
+		const DWORD m_style;
+		DWORD m_listViewExStyle = 0;
+		CImageList m_imageLists[4];
+		bool m_groupViewEnabled = false;
+	public:
+		CListControl_ListViewBase(DWORD style) : m_style(style) {
+			if (style & LVS_SINGLESEL) this->SetSelectionModeSingle();
+			else this->SetSelectionModeMulti();
+		}
+
+		BEGIN_MSG_MAP_EX(CListControl_ListViewBase)
+			MSG_WM_CREATE(OnCreate)
+			MESSAGE_HANDLER_EX(LVM_INSERTCOLUMN, OnInsertColumn)
+			MESSAGE_HANDLER_EX(LVM_DELETECOLUMN, OnDeleteColumn)
+			MESSAGE_HANDLER_EX(LVM_SETCOLUMN, OnSetColumn)
+			MESSAGE_HANDLER_EX(LVM_SETCOLUMNWIDTH, OnSetColumnWidth)
+			MESSAGE_HANDLER_EX(LVM_GETCOLUMNWIDTH, OnGetColumnWidth)
+			MESSAGE_HANDLER_EX(LVM_GETCOLUMNORDERARRAY, OnGetColumnOrderArray)
+			MESSAGE_HANDLER_EX(LVM_SETCOLUMNORDERARRAY, OnSetColumnOrderArray)
+			MESSAGE_HANDLER_EX(LVM_GETITEMCOUNT, OnGetItemCount)
+			MESSAGE_HANDLER_EX(LVM_GETITEMRECT, OnGetItemRect)
+			MESSAGE_HANDLER_EX(LVM_GETSUBITEMRECT, OnGetSubItemRect)
+			MESSAGE_HANDLER_EX(LVM_HITTEST, OnHitTest)
+			MESSAGE_HANDLER_EX(LVM_GETITEMSTATE, OnGetItemState)
+			MESSAGE_HANDLER_EX(LVM_SETITEMSTATE, OnSetItemState)
+			MESSAGE_HANDLER_EX(LVM_GETNEXTITEM, OnGetNextItem)
+			MESSAGE_HANDLER_EX(LVM_GETNEXTITEMINDEX, OnGetNextItemIndex)
+			MESSAGE_HANDLER_EX(LVM_SUBITEMHITTEST, OnSubItemHitTest)
+			MESSAGE_HANDLER_EX(LVM_GETSELECTEDCOUNT, OnGetSelCount)
+			MESSAGE_HANDLER_EX(LVM_GETHEADER, OnGetHeader)
+			MESSAGE_HANDLER_EX(LVM_SETEXTENDEDLISTVIEWSTYLE, OnSetExtendedListViewStyle)
+			MESSAGE_HANDLER_EX(LVM_GETEXTENDEDLISTVIEWSTYLE, OnGetExtendedListViewStyle)
+			MESSAGE_HANDLER_EX(LVM_ENSUREVISIBLE, OnEnsureVisible)
+			MESSAGE_HANDLER_EX(LVM_SETIMAGELIST, OnSetImageList)
+			MESSAGE_HANDLER_EX(LVM_GETIMAGELIST, OnGetImageList)
+			MESSAGE_HANDLER_EX(LVM_EDITLABEL, OnEditLabel)
+			MESSAGE_HANDLER_EX(LVM_GETSTRINGWIDTH, OnGetStringWidth)
+			MESSAGE_HANDLER_EX(LVM_ENABLEGROUPVIEW, OnEnableGroupView)
+			MESSAGE_HANDLER_EX(LVM_SCROLL, OnScroll)
+			MESSAGE_HANDLER_EX(LVM_REDRAWITEMS, OnRedrawItems)
+			MSG_WM_KEYDOWN(OnKeyDown)
+			MSG_WM_SYSKEYDOWN(OnKeyDown)
+			CHAIN_MSG_MAP(CListControlComplete)
+		END_MSG_MAP()
+
+		void OnKeyDown(UINT nChar, UINT nRepCnt, UINT nFlags) {
+			NMLVKEYDOWN arg = {};
+			arg.hdr = this->setupHdr(LVN_KEYDOWN);
+			arg.wVKey = nChar;
+			sendNotify(&arg);
+			SetMsgHandled(FALSE);
+		}
+		LRESULT OnCreate(LPCREATESTRUCT) {
+			SetMsgHandled(FALSE);
+			// Adopt style flags of the original control to keep various ATL checks happy
+			DWORD style = GetStyle();
+			style = (style & 0xFFFF0000) | (m_style & 0xFFFF);
+			SetWindowLong(GWL_STYLE, style);
+			return 0;
+		}
+
+		LRESULT OnGetColumnWidth(UINT, WPARAM wp, LPARAM) {
+			size_t idx = (size_t)wp;
+			return GetSubItemWidth(idx);
+		}
+		LRESULT OnSetColumnWidth(UINT, WPARAM wp, LPARAM lp) {
+			size_t idx = (size_t)wp;
+			if (idx >= GetColumnCount()) return FALSE;
+			// undocumented: low 16 bits must be used, or else testing against LVSCW_AUTOSIZE fails hard
+			// all macros sending LVM_SETCOLUMNWIDTH use low 16 bits of LPARAM
+			int w = (short)lp; 
+			if (this->IsHeaderless()) {
+				if (w < 0) {
+					m_headerlessColumns[idx] = UINT32_MAX;
+				} else {
+					m_headerlessColumns[idx] = (uint32_t)w;
+					this->OnColumnsChanged();
+				}
+			} else {
+				uint32_t pass = (uint32_t)w;
+				if (w < 0) {
+					if (w == LVSCW_AUTOSIZE_USEHEADER) pass = columnWidthAuto;
+					else pass = columnWidthAutoUseContent;
+				}
+				this->ResizeColumn(idx, pass);
+			}
+			return TRUE;
+		}
+		LRESULT OnGetColumnOrderArray(UINT, WPARAM wp, LPARAM lp) {
+			int* out = (int*)lp;
+			auto arr = this->GetColumnOrderArray();
+			if ((size_t)wp != arr.size()) return 0;
+			for (size_t w = 0; w < arr.size(); ++w) out[w] = arr[w];
+			return 1;
+		}
+		LRESULT OnSetColumnOrderArray(UINT, WPARAM wp, LPARAM lp) {
+			if (this->IsHeaderless()) {
+				PFC_ASSERT(!"Implement and test me");
+				return 0;
+			} else {
+				auto hdr = GetHeaderCtrl();
+				WIN32_OP_D(hdr.SetOrderArray((int)wp, (int*)lp));
+			}
+			ReloadData();
+			return 1;
+		}
+		LRESULT OnInsertColumn(UINT, WPARAM wp, LPARAM lp) {
+			size_t idx = (size_t)wp;
+			auto col = reinterpret_cast<LVCOLUMN*>(lp);
+
+			if (IsHeaderless()) {
+				if (idx > m_headerlessColumns.size()) idx = m_headerlessColumns.size();
+				uint32_t width = 0;
+				if (col->mask & LVCF_WIDTH) width = col->cx;
+				m_headerlessColumns.insert(m_headerlessColumns.begin() + idx, width);
+				this->OnColumnsChanged();
+				return 0;
+			}
+			if (this->GetHeaderCtrl() == NULL) {
+				if (m_style & LVS_NOSORTHEADER) {
+					this->InitializeHeaderCtrl();
+				} else {
+					this->InitializeHeaderCtrlSortable();
+				}
+			}
+
+			PFC_ASSERT(this->GetHeaderCtrl().GetItemCount() == (int)this->GetColumnCount());
+
+			if (idx != this->GetColumnCount()) {
+				PFC_ASSERT(!"Arbitrary column insert not implemented, please add columns in order");
+				return -1;
+			}
+
+			pfc::string8 label; int width = 0; DWORD format = HDF_LEFT;
+			if (col->mask & LVCF_TEXT) label = pfc::utf8FromWide(col->pszText);
+			if (col->mask & LVCF_WIDTH) width = col->cx;
+			if (col->mask & LVCF_FMT) {
+				format = col->fmt & LVCFMT_JUSTIFYMASK;
+			}
+			this->AddColumn(label, width, format);
+
+			PFC_ASSERT(this->GetHeaderCtrl().GetItemCount() == (int)this->GetColumnCount());
+
+			return idx;
+		}
+		LRESULT OnDeleteColumn(UINT, WPARAM wp, LPARAM) {
+			size_t idx = (size_t)wp;
+			if (idx >= this->GetColumnCount()) {
+				PFC_ASSERT(!"???"); return FALSE;
+			}
+			this->DeleteColumn(idx);
+			return TRUE;
+		}
+		LRESULT OnSetColumn(UINT, WPARAM wp, LPARAM lp) {
+			size_t idx = (size_t)wp;
+			if (idx >= this->GetColumnCount()) {
+				PFC_ASSERT(!"???"); return FALSE;
+			}
+			auto col = reinterpret_cast<LVCOLUMN*>(lp);
+			
+			if (col->mask & (LVCF_TEXT | LVCF_FMT)) {
+				const char* pText = nullptr; DWORD format = UINT32_MAX;
+				pfc::string8 strText;
+				if (col->mask & LVCF_TEXT) {
+					strText = pfc::utf8FromWide(col->pszText);
+					pText = strText.c_str();
+				}
+				if (col->mask & LVCF_FMT) {
+					format = col->fmt & LVCFMT_JUSTIFYMASK;
+				}
+				this->SetColumn(idx, pText, format);
+			}
+
+			if (col->mask & LVCF_WIDTH) {
+				this->ResizeColumn(idx, col->cx);
+			}
+
+			return TRUE;
+		}
+		LRESULT OnGetItemCount(UINT, WPARAM, LPARAM) {
+			return (LRESULT)this->GetItemCount();
+		}
+
+		LRESULT OnGetItemRect(UINT, WPARAM wp, LPARAM lp) {
+			size_t idx = (size_t)wp;
+			RECT* rc = (RECT*)lp;
+			if (idx < GetItemCount()) {
+				// LVIR_* not supported
+				* rc = GetItemRect(idx);
+				return TRUE;
+			}
+			return FALSE;
+		}
+
+		LRESULT OnGetSubItemRect(UINT, WPARAM wp, LPARAM lp) {
+			size_t idx = (size_t)wp;
+			RECT* rc = (RECT*)lp;
+			if (idx < GetItemCount()) {
+				size_t subItem = (size_t)rc->top;
+				// LVIR_* not supported
+				*rc = GetSubItemRect(idx, subItem);
+				return TRUE;
+			}
+			return FALSE;
+		}
+
+		LRESULT OnHitTest(UINT, WPARAM, LPARAM lp) {
+			auto info = (LVHITTESTINFO*)lp;
+			CPoint pt = this->PointClientToAbs(info->pt);
+			size_t item = this->ItemFromPointAbs(pt);
+			size_t subItem = SIZE_MAX;
+			if (item != SIZE_MAX) {
+				info->iItem = (int)item;
+				size_t subItem = this->SubItemFromPointAbs(pt);
+				info->iSubItem = (subItem != SIZE_MAX) ? (int)subItem : -1;
+			}
+			return item != SIZE_MAX ? (LRESULT)item : (LRESULT)-1;
+		}
+		LRESULT OnSubItemHitTest(UINT, WPARAM, LPARAM lp) {
+			auto info = (LVHITTESTINFO*)lp;
+			CPoint pt = this->PointClientToAbs(info->pt);
+			size_t item = this->ItemFromPointAbs(pt);
+			size_t subItem = SIZE_MAX;
+			if (item != SIZE_MAX) {
+				info->iItem = (int)item;
+				subItem = this->SubItemFromPointAbs(pt);
+				info->iSubItem = (subItem != SIZE_MAX) ? (int)subItem : -1;
+			}
+			return subItem != SIZE_MAX ? (LRESULT)subItem : (LRESULT)-1;
+		}
+
+		virtual void SetItemState(size_t idx, DWORD mask, DWORD state) {
+			if (idx >= GetItemCount()) return;
+			if (mask & LVIS_FOCUSED) {
+				if (state & LVIS_FOCUSED) {
+					this->SetFocusItem(idx);
+				} else {
+					if (this->GetFocusItem() == idx) {
+						this->SetFocusItem(SIZE_MAX);
+					}
+				}
+			}
+			if (mask & LVIS_SELECTED) {
+				if (this->IsSingleSelect()) {
+					if (state & LVIS_SELECTED) this->SelectSingle(idx);
+				} else {
+					this->SetSelectionAt(idx, (state & LVIS_SELECTED) != 0);
+				}
+			}
+		}
+		LRESULT OnSetItemState(UINT, WPARAM wp, LPARAM lp) {
+			LVITEM* pItem = (LVITEM*)lp;
+			if ((LONG_PTR)wp < 0) {
+				if (pItem->stateMask & LVIS_FOCUSED) {
+					if (pItem->state & LVIS_FOCUSED) {
+						// makes no sense
+					} else {
+						this->SetFocusItem(SIZE_MAX);
+					}
+				}
+				if (pItem->stateMask & LVIS_SELECTED) {
+					if (pItem->state & LVIS_SELECTED) {
+						this->SelectAll();
+					} else {
+						this->SelectNone();
+					}
+				}
+			} else {
+				size_t idx = (size_t)wp;
+				SetItemState(idx, pItem->stateMask, pItem->state);
+			}
+			return TRUE;
+		}
+
+		virtual UINT GetItemState(size_t idx) const {
+			UINT ret = 0;
+			if (idx == this->GetFocusItem()) ret |= LVIS_FOCUSED;
+			if (this->IsItemSelected(idx)) ret |= LVIS_SELECTED;
+			return ret;
+		}
+		LRESULT OnGetItemState(UINT, WPARAM wp, LPARAM lp) {
+			LRESULT ret = 0;
+			size_t idx = (size_t)wp;
+			if (idx < GetItemCount()) {
+				ret |= (this->GetItemState(idx) & lp);
+			}
+			return ret;
+		}
+		
+		LRESULT OnGetNextItemIndex(UINT, WPARAM wp, LPARAM lp) {
+			LVITEMINDEX* info = (LVITEMINDEX*)wp;
+			int item = (int)OnGetNextItem(LVM_GETNEXTITEM, info->iItem, lp);
+			info->iItem = item;
+			return item >= 0;
+		}
+		LRESULT OnGetNextItem(UINT, WPARAM wp, LPARAM lp) {
+			// GetSelectedIndex() :
+			// return (int)::SendMessage(this->m_hWnd, LVM_GETNEXTITEM, (WPARAM)-1, MAKELPARAM(LVNI_ALL | LVNI_SELECTED, 0));
+			
+			const bool bVisible = (lp & LVNI_VISIBLEONLY) != 0;
+			std::pair<size_t, size_t> range;
+			if ( bVisible ) range = this->GetVisibleRange();
+
+			if ((lp & LVNI_STATEMASK) == LVNI_FOCUSED) {
+				size_t ret = this->GetFocusItem();
+				if (bVisible) {
+					if (ret < range.first || ret >= range.second) ret = SIZE_MAX;
+				}
+				return (LRESULT)ret;
+			}
+
+			const auto count = this->GetItemCount();
+
+			if (wp == (WPARAM)-1 && bVisible) {
+				// Simplified visible range search
+				for (size_t idx = range.first; idx < range.second; ++idx) {
+					if (this->GetItemState(idx) & lp) {
+						return (LRESULT)idx;
+					}
+				}
+				return -1;
+			}
+			
+			size_t base;
+			if ((LONG_PTR)wp < 0) base = 0;
+			else base = wp + 1;
+			for (size_t idx = base; idx < count; ++idx) {
+				if (bVisible) {
+					if (idx < range.first || idx >= range.second) continue;
+				}
+				if (this->GetItemState(idx) & lp) {
+					return (LRESULT)idx;
+				}
+			}
+			return -1;
+		}
+		LRESULT OnGetSelCount(UINT, WPARAM, LPARAM) {
+			return (LRESULT) this->GetSelectedCount();
+		}
+		LRESULT OnGetHeader(UINT, WPARAM, LPARAM) {
+			return (LRESULT)GetHeaderCtrl().m_hWnd;
+		}
+		LRESULT OnSetExtendedListViewStyle(UINT, WPARAM wp, LPARAM lp) {
+			DWORD ret = m_listViewExStyle;
+			DWORD set = (DWORD)lp, mask = (DWORD)wp;
+			if (mask == 0) mask = 0xFFFFFFFF;
+			m_listViewExStyle = (m_listViewExStyle & ~mask) | (set & mask);
+
+			this->SetRowStyle((m_listViewExStyle & LVS_EX_GRIDLINES) ? rowStyleGrid : rowStyleDefault);
+
+			if (m_listViewExStyle != ret) ReloadData();
+			return ret;
+		}
+		LRESULT OnGetExtendedListViewStyle(UINT, WPARAM, LPARAM) {
+			return m_listViewExStyle;
+		}
+		LRESULT OnEnsureVisible(UINT, WPARAM wp, LPARAM lp) {
+			size_t idx = (size_t)wp;
+			if (idx < this->GetItemCount()) {
+				(void)lp; // FIX ME partialOK?
+				this->EnsureItemVisible(idx);
+				return TRUE;
+			}
+			return FALSE;
+		}
+		LRESULT OnSetImageList(UINT, WPARAM wp, LPARAM lp) {
+			size_t idx = (size_t)wp;
+			LRESULT ret = 0;
+			if (idx < std::size(m_imageLists)) {
+				auto& ref = m_imageLists[idx];
+				ret = (LRESULT)ref.m_hImageList;
+				ref = (HIMAGELIST)lp;
+			}
+			return ret;
+		}
+		LRESULT OnGetImageList(UINT, WPARAM wp, LPARAM lp) {
+			size_t idx = (size_t)wp;
+			LRESULT ret = 0;
+			if (idx < std::size(m_imageLists)) {
+				ret = (LRESULT)m_imageLists[idx].m_hImageList;
+			}
+			return ret;
+		}
+		LRESULT OnEditLabel(UINT, WPARAM wp, LPARAM) {
+			int index = (int)wp;
+			HWND ret = NULL;
+			if (index < 0) {
+				this->TableEdit_Abort(false);
+			}  else {
+				ret = this->TableEdit_Start((size_t)index, 0);
+			}
+			return (LRESULT)ret;
+		}
+		LRESULT OnGetStringWidth(UINT, WPARAM, LPARAM lp) {
+			auto str = (const wchar_t*)lp;
+			return this->GetOptimalColumnWidthFixed(pfc::utf8FromWide(str), false /* no pad */);
+		}
+		LRESULT OnEnableGroupView(UINT, WPARAM wp, LPARAM) {
+			bool bEnable = (wp != 0);
+			if (bEnable == m_groupViewEnabled) return 0;
+			m_groupViewEnabled = bEnable;
+			ReloadData();
+			return 1;
+		}
+		LRESULT OnScroll(UINT, WPARAM, LPARAM) {
+			PFC_ASSERT(!"Implement me");
+			return 0;
+		}
+		LRESULT OnRedrawItems(UINT, WPARAM wp, LPARAM lp) {
+			size_t base = wp;
+			size_t last = lp;
+			if (last < base) return FALSE;
+			size_t count = last + 1 - base;
+			this->UpdateItems(pfc::bit_array_range(base, count));
+			return TRUE;
+		}
+		bool TableEdit_CanAdvanceHere(size_t item, size_t subItem, uint32_t whatHappened) const override {
+			// Block line cycling with enter key
+			auto code = (whatHappened & InPlaceEdit::KEditMaskReason);
+			if (code == InPlaceEdit::KEditEnter) return false;
+			return __super::TableEdit_CanAdvanceHere(item, subItem, whatHappened);
+		}
+		bool TableEdit_IsColumnEditable(t_size subItem) const override {
+			return subItem == 0;
+		}
+		NMHDR setupHdr(UINT code) const {
+			return { m_hWnd, (UINT_PTR)this->GetDlgCtrlID(), code };
+		}
+		LRESULT sendNotify(void* data) const {
+			auto hdr = reinterpret_cast<const NMHDR*>(data);
+			PFC_ASSERT(hdr->idFrom == (UINT_PTR) GetDlgCtrlID());
+			return GetParent().SendMessage(WM_NOTIFY, hdr->idFrom, reinterpret_cast<LPARAM>(data));
+		}
+		int getDispInfoImage(size_t item, size_t subItem) const {
+			NMLVDISPINFO info = { setupHdr(LVN_GETDISPINFO) };
+			info.item.mask = LVIF_IMAGE;
+			info.item.iItem = (int)item;
+			info.item.iSubItem = (int)subItem;
+			sendNotify(&info);
+			PFC_ASSERT(info.item.iImage != I_IMAGECALLBACK);
+			return info.item.iImage;
+		}
+		pfc::string8 getDispInfoText(size_t item, size_t subItem) const {
+			NMLVDISPINFO info = { setupHdr(LVN_GETDISPINFO) };
+			info.item.mask = LVIF_TEXT;
+			info.item.iItem = (int)item;
+			info.item.iSubItem = (int)subItem;
+
+			wchar_t buffer[1024] = {};
+			info.item.pszText = buffer;
+			info.item.cchTextMax = (int) std::size(buffer);
+			sendNotify(&info);
+			if (info.item.pszText == LPSTR_TEXTCALLBACK || info.item.pszText == nullptr) {
+				PFC_ASSERT(!"WTF??"); return "";
+			}
+			return pfc::utf8FromWide(info.item.pszText);
+		}
+		virtual LPARAM GetItemParam(size_t) { return 0; }
+		void ExecuteDefaultAction(size_t idx) override {
+			NMITEMACTIVATE info = { setupHdr(NM_DBLCLK) };
+			info.iItem = (int) idx;
+			info.lParam = GetItemParam(idx);
+			sendNotify(&info);
+		}
+		void OnSubItemClicked(t_size item, t_size subItem, CPoint pt) override {
+			__super::OnSubItemClicked(item, subItem, pt);
+			NMITEMACTIVATE info = { setupHdr(NM_CLICK) };
+			info.iItem = (int)item;
+			info.iSubItem = (int)subItem;
+			info.ptAction = pt;
+			sendNotify(&info);
+		}
+		void OnFocusChanged(size_t oldFocus, size_t newFocus) override {
+			__super::OnFocusChanged(oldFocus, newFocus);
+			const auto count = this->GetItemCount();
+
+			if (oldFocus < count) {
+				auto idx = oldFocus;
+				NMLISTVIEW info = { setupHdr(LVN_ITEMCHANGED) };
+				info.iItem = (int)idx;
+				info.lParam = this->GetItemParam(idx);
+				auto base = this->GetItemState(idx);
+				info.uOldState = base | LVIS_FOCUSED;
+				info.uNewState = base;
+				info.uChanged = LVIF_STATE;
+				this->sendNotify(&info);
+			}
+			if (newFocus < count) {
+				auto idx = newFocus;
+				NMLISTVIEW info = { setupHdr(LVN_ITEMCHANGED) };
+				info.iItem = (int)idx;
+				info.lParam = this->GetItemParam(idx);
+				auto base = this->GetItemState(idx);
+				info.uOldState = base & ~LVIS_FOCUSED;
+				info.uNewState = base;
+				info.uChanged = LVIF_STATE;
+				this->sendNotify(&info);
+			}
+		}
+
+		void OnSelectionChanged(pfc::bit_array const& affected, pfc::bit_array const& status) override {
+
+			__super::OnSelectionChanged(affected, status);
+			const auto count = this->GetItemCount();
+
+			const size_t focus = this->GetFocusItem();
+
+			// LVN_ITEMCHANGING not supported, CListControl currently doesn't hand this info
+
+			affected.for_each(true, 0, count, [&](size_t idx) {
+				bool sel_new = status[idx];
+				bool sel_old = !sel_new;
+				NMLISTVIEW info = { setupHdr(LVN_ITEMCHANGED) };
+				info.iItem = (int)idx;
+				info.lParam = this->GetItemParam(idx);
+				info.uOldState = (sel_old ? LVIS_SELECTED : 0) | (idx == focus ? LVIS_FOCUSED : 0);
+				info.uNewState = (sel_new ? LVIS_SELECTED : 0) | (idx == focus ? LVIS_FOCUSED : 0);
+				info.uChanged = LVIF_STATE;
+				this->sendNotify(&info);
+			});
+		}
+		void RequestReorder(size_t const* order, size_t count) override {}
+		void RequestRemoveSelection() override {}
+
+		void OnColumnHeaderClick(t_size index) override {
+			NMLISTVIEW info = { setupHdr(LVN_COLUMNCLICK) };
+			info.iItem = -1;
+			info.iSubItem = (int)index;
+			this->sendNotify(&info);
+		}
+
+		bool IsHeaderless() const {
+			return (m_style & LVS_NOCOLUMNHEADER) != 0;
+		}
+
+		std::vector< uint32_t > m_headerlessColumns;
+
+		size_t GetColumnCount() const override {
+			if (IsHeaderless()) {
+				return m_headerlessColumns.size();
+			}
+			return __super::GetColumnCount();
+		}
+		uint32_t GetSubItemWidth(size_t subItem) const override {
+			if (IsHeaderless()) {
+				if (subItem < m_headerlessColumns.size()) {
+					auto v = m_headerlessColumns[subItem];
+					if (v == UINT32_MAX) {
+						uint32_t nAuto = 0, wNormal = 0;
+						for (auto walk : m_headerlessColumns) {
+							if (walk == UINT32_MAX) ++nAuto;
+							else wNormal += walk;
+						}
+						PFC_ASSERT(nAuto > 0);
+						uint32_t wTotal = this->GetClientRectHook().Width();
+						if (wTotal > wNormal) {
+							uint32_t autoTotal = wTotal - wNormal;
+							v = autoTotal / nAuto;
+						} else {
+							v = 0;
+						}
+					}
+					return v;
+				}
+				else return 0;
+			}
+			return __super::GetSubItemWidth(subItem);
+		}
+
+		bool GetCellTypeSupported() const override { 
+			return useCheckBoxes();
+		}
+
+		cellType_t GetCellType(size_t item, size_t subItem) const override {
+			if (useCheckBoxes() && subItem == 0) {
+				return &PFC_SINGLETON(CListCell_Checkbox);
+			}
+			return __super::GetCellType(item, subItem);
+		}
+
+		bool useCheckBoxes() const {
+			return (m_listViewExStyle & LVS_EX_CHECKBOXES) != 0;
+		}
+
+		void TableEdit_SetField(t_size item, t_size subItem, const char* value) override {
+			if (subItem != 0) {
+				PFC_ASSERT(!"subItem should be zero");
+				return;
+			}
+			auto textW = pfc::wideFromUTF8(value);
+			NMLVDISPINFO info = { setupHdr(LVN_ENDLABELEDIT) };
+			info.item.iItem = (int)item;
+			info.item.mask = LVIF_TEXT;
+			info.item.pszText = const_cast<wchar_t*>( textW.c_str() );
+
+			if (sendNotify(&info) != 0) {
+				SetSubItemText(item, subItem, value);
+			}
+		}
+
+		virtual void SetSubItemText(size_t item, size_t subItem, const char* text) {}
+
+		virtual int GetItemImage(size_t item, size_t subItem) const {
+			return I_IMAGEREALLYNONE;
+		}
+		CImageList GetImageList() const {
+			return m_imageLists[LVSIL_SMALL];
+		}
+		bool RenderCellImageTest(size_t item, size_t subItem) const override { 
+			return GetImageList() != NULL && GetItemImage(item, subItem) != I_IMAGEREALLYNONE;
+		}
+		void RenderCellImage(size_t item, size_t subItem, CDCHandle dc, const CRect& rc) const override {
+			auto img = GetItemImage(item, subItem);
+			auto imgList = GetImageList();
+			if (img >= 0 && imgList) {
+				CSize size;
+				WIN32_OP_D(imgList.GetIconSize(size));
+				CRect rc2 = rc;
+				if (size.cx <= rc.Width() && size.cy <= rc.Height()) {
+					auto cp = rc.CenterPoint();
+					rc2.left = cp.x - size.cx / 2;
+					rc2.top = cp.y - size.cy / 2;
+					rc2.right = rc2.left + size.cx;
+					rc2.bottom = rc2.top + size.cy;
+				}
+				imgList.DrawEx(img, dc, rc2, CLR_NONE, CLR_NONE, ILD_SCALE);
+			}
+		}
+
+		bool m_notifyItemDraw = false;
+
+		UINT GetItemCDState(size_t which) const {
+			UINT ret = 0;
+			DWORD state = GetItemState(which);
+			if (state & LVIS_FOCUSED) ret |= CDIS_FOCUS;
+			if (state & LVIS_SELECTED) ret |= CDIS_SELECTED;
+			return ret;
+		}
+
+		void RenderRect(const CRect& p_rect, CDCHandle p_dc) override {
+			NMCUSTOMDRAW cd = { setupHdr(NM_CUSTOMDRAW) };
+			cd.dwDrawStage = CDDS_PREPAINT;
+			cd.hdc = p_dc;
+			cd.rc = p_rect;
+			cd.dwItemSpec = UINT32_MAX;
+			cd.uItemState = 0;
+			cd.lItemlParam = 0;
+			LRESULT status = sendNotify(&cd);
+			m_notifyItemDraw = (status & CDRF_NOTIFYITEMDRAW) != 0;
+
+			if ((status & CDRF_SKIPDEFAULT) != 0) {
+				return;
+			}
+			__super::RenderRect(p_rect, p_dc);
+
+			cd.dwDrawStage = CDDS_POSTPAINT;
+			sendNotify(&cd);
+		}
+		void RenderItem(t_size item, const CRect& itemRect, const CRect& updateRect, CDCHandle dc) override {
+			NMCUSTOMDRAW cd = {};
+			if (m_notifyItemDraw) {
+				cd = { setupHdr(NM_CUSTOMDRAW) };
+				cd.dwDrawStage = CDDS_ITEMPREPAINT;
+				cd.hdc = dc;
+				cd.rc = itemRect;
+				cd.dwItemSpec = (DWORD)item;
+				cd.uItemState = GetItemCDState(item);
+				cd.lItemlParam = GetItemParam(item);
+				LRESULT status = sendNotify(&cd);
+				if (status & CDRF_SKIPDEFAULT) return;
+			}
+			
+			__super::RenderItem(item, itemRect, updateRect, dc);
+
+
+			if (m_notifyItemDraw) {
+				cd.dwDrawStage = CDDS_ITEMPOSTPAINT;
+				sendNotify(&cd);
+			}
+		}
+#if 0
+		void RenderSubItemText(t_size item, t_size subItem, const CRect& subItemRect, const CRect& updateRect, CDCHandle dc, bool allowColors) override {
+
+			__super::RenderSubItemText(item, subItem, subItemRect, updateRect, dc, allowColors);
+		}
+#endif
+
+	};
+
+	class CListControl_ListViewOwnerData : public CListControl_ListViewBase {
+	public:
+		CListControl_ListViewOwnerData(DWORD style) : CListControl_ListViewBase(style) {}
+
+		BEGIN_MSG_MAP_EX(CListControl_ListViewOwnerData)
+			MESSAGE_HANDLER_EX(LVM_SETITEMCOUNT, OnSetItemCount)
+			CHAIN_MSG_MAP(CListControl_ListViewBase)
+		END_MSG_MAP()
+	private:
+		size_t m_itemCount = 0;
+		LRESULT OnSetItemCount(UINT, WPARAM wp, LPARAM) {
+			auto count = (size_t)wp;
+			if (m_itemCount != count) {
+				m_itemCount = count; ReloadData();
+			}
+			return 0;
+		}
+
+		t_size GetItemCount() const override {
+			return m_itemCount;
+		}
+		bool GetSubItemText(t_size item, t_size subItem, pfc::string_base& out) const override {
+			if (item < m_itemCount) {
+				out = this->getDispInfoText(item, subItem);
+				return true;
+			}
+			return false;
+		}
+		int GetItemImage(size_t item, size_t subItem) const override {
+			int ret = I_IMAGEREALLYNONE;
+			if (subItem == 0) {
+				ret = this->getDispInfoImage(item, subItem);
+			}
+			return ret;
+		}
+		void SetSubItemText(size_t item, size_t subItem, const char* text) override {
+			auto textW = pfc::wideFromUTF8(text);
+			NMLVDISPINFO info = { setupHdr(LVN_SETDISPINFO) };
+			info.item.iItem = (int)item;
+			info.item.iSubItem = (int)subItem;
+			info.item.mask = LVIF_TEXT;
+			info.item.pszText = const_cast<wchar_t*>(textW.c_str());
+			sendNotify(&info);
+			ReloadItem(item);
+		}
+	};
+
+	class CListControl_ListView : public CListControl_ListViewBase {
+	public:
+		CListControl_ListView(DWORD style) : CListControl_ListViewBase(style) {}
+
+		BEGIN_MSG_MAP_EX(CListControl_ListView)
+			MESSAGE_HANDLER_EX(LVM_INSERTITEM, OnInsertItem)
+			MESSAGE_HANDLER_EX(LVM_SETITEM, OnSetItem)
+			MESSAGE_HANDLER_EX(LVM_SETITEMCOUNT, OnSetItemCount)
+			MESSAGE_HANDLER_EX(LVM_GETITEM, OnGetItem)
+			MESSAGE_HANDLER_EX(LVM_GETITEMTEXT, OnGetItemText)
+			MESSAGE_HANDLER_EX(LVM_INSERTGROUP, OnInsertGroup)
+			MESSAGE_HANDLER_EX(LVM_REMOVEGROUP, OnRemoveGroup)
+			MESSAGE_HANDLER_EX(LVM_GETGROUPCOUNT, OnGetGroupCount)
+			MESSAGE_HANDLER_EX(LVM_GETGROUPINFO, OnGetGroupInfo)
+			MESSAGE_HANDLER_EX(LVM_SETGROUPINFO, OnSetGroupInfo)
+			MESSAGE_HANDLER_EX(LVM_GETGROUPINFOBYINDEX, OnGetGroupInfoByIndex)
+			MESSAGE_HANDLER_EX(LVM_REMOVEALLGROUPS, OnRemoveAllGroups)
+			MESSAGE_HANDLER_EX(LVM_DELETEITEM, OnDeleteItem)
+			MESSAGE_HANDLER_EX(LVM_DELETEALLITEMS, OnDeleteAllItems)
+			CHAIN_MSG_MAP(CListControl_ListViewBase)
+		END_MSG_MAP()
+	private:
+		struct text_t {
+			pfc::string8 text;
+			bool callback = false;
+		};
+		struct rec_t {
+			std::vector< text_t > text;
+			LPARAM param = 0;
+			int image = I_IMAGEREALLYNONE;
+			bool checked = false;
+			groupID_t groupID = 0;
+			int LVGroupID = 0;
+		};
+
+		std::vector<rec_t> m_content;
+
+		groupID_t m_groupIDGen = 0;
+		groupID_t groupIDGen() { return ++m_groupIDGen; }
+		struct group_t {
+			int LVGroupID = 0;
+			groupID_t GroupID = 0;
+			pfc::string8 header;
+		};
+		std::vector<group_t> m_groups;
+
+		groupID_t groupFromLV(int LV) const {
+			for (auto& g : m_groups) {
+				if (g.LVGroupID == LV) return g.GroupID;
+			}
+			return 0;
+		}
+
+		t_size GetItemCount() const override {
+			return m_content.size();
+		}
+		bool GetSubItemText(t_size item, t_size subItem, pfc::string_base& out) const override {
+			if (item < m_content.size()) {
+				auto& r = m_content[item];
+				if (subItem < r.text.size()) {
+					auto& t = r.text[subItem];
+					if (t.callback) {
+						out = getDispInfoText(item, subItem);
+					} else {
+						out = t.text;
+					}
+					return true;
+				}
+			}
+			return false;
+		}
+		void SetSubItemText(size_t item, size_t subItem, const char* text) override {
+			if (item < m_content.size()) {
+				auto& r = m_content[item];
+				if (subItem < r.text.size()) {
+					auto& t = r.text[subItem];
+					t.callback = false; t.text = text;
+					ReloadItem(item);
+				}
+			}
+		}
+
+		int GetItemImage(size_t item, size_t subItem) const override {
+			int ret = I_IMAGEREALLYNONE;
+			if (subItem == 0 && item < m_content.size()) {
+				auto& r = m_content[item];
+				ret = r.image;
+				if (ret == I_IMAGECALLBACK) ret = this->getDispInfoImage(item, subItem);
+			}
+			return ret;
+		}
+		LPARAM GetItemParam(size_t item) override {
+			LPARAM ret = 0;
+			if (item < m_content.size()) {
+				ret = m_content[item].param;
+			}
+			return ret;
+		}
+		
+		static text_t makeText(const wchar_t* p) {
+			text_t text;
+			if (p == LPSTR_TEXTCALLBACK) {
+				text.callback = true;
+			} else {
+				text.text = pfc::utf8FromWide(p);
+			}
+			return text;
+		}
+		LRESULT OnSetItem(UINT, WPARAM, LPARAM lp) {
+			auto pItem = reinterpret_cast<LVITEM*>(lp);
+			size_t item = (size_t)pItem->iItem;
+			const size_t total = GetItemCount();
+			if (item >= total) return FALSE;
+			size_t subItem = (size_t)pItem->iSubItem;
+			if (subItem > 1024) return FALSE; // quick sanity
+			auto& rec = m_content[item];
+			size_t width = subItem + 1;
+			if (rec.text.size() < width) rec.text.resize(width);
+			bool bReload = false, bReloadAll = false;
+			if (pItem->mask & LVIF_TEXT) {
+				rec.text[subItem] = makeText(pItem->pszText);
+				bReload = true;
+			}
+			if (pItem->mask & LVIF_IMAGE) {
+				rec.image = pItem->iImage;
+				bReload = true;
+			}
+			if (pItem->mask & LVIF_PARAM) {
+				rec.param = pItem->lParam;
+			}
+			if (pItem->mask & LVIF_GROUPID) {
+				rec.LVGroupID = pItem->iGroupId;
+				rec.groupID = groupFromLV(rec.LVGroupID);
+				if (m_groupViewEnabled) bReloadAll = true;
+			}
+			if (bReloadAll) {
+				this->ReloadData();
+			} else if (bReload) {
+				this->ReloadItem(item);
+			}
+			if (pItem->mask & LVIF_STATE) {
+				this->SetItemState(item, pItem->stateMask, pItem->state);
+			}
+			return TRUE;
+		}
+		LRESULT OnInsertItem(UINT, WPARAM, LPARAM lp) {
+			auto pItem = reinterpret_cast<LVITEM*>(lp);
+
+			size_t item = (size_t)pItem->iItem;
+			const size_t total = GetItemCount();
+			if (item > total) item = total;
+
+			rec_t r;
+			if (pItem->mask & LVIF_TEXT) {
+				r.text = { makeText(pItem->pszText) };
+			}
+			if (pItem->mask & LVIF_GROUPID) {
+				r.LVGroupID = pItem->iGroupId;
+				r.groupID = groupFromLV(r.LVGroupID);
+			}
+			if (pItem->mask & LVIF_IMAGE) {
+				r.image = pItem->iImage;
+			}
+			if (pItem->mask & LVIF_PARAM) {
+				r.param = pItem->lParam;
+			}
+
+			m_content.insert(m_content.begin() + item, std::move(r));
+			if (m_groupViewEnabled) ReloadData();
+			else this->OnItemsInserted(item, 1, false);
+			
+			return (LRESULT)item;
+		}
+		LRESULT OnSetItemCount(UINT, WPARAM wp, LPARAM) {
+			// It's not actually supposed to be called like this, LVM_SETITEMCOUNT is meant for ownerdata listviews
+			auto count = (size_t)wp;
+			if (count != m_content.size()) {
+				m_content.resize(wp);
+				ReloadData();
+			}
+			return 0;
+		}
+
+		LRESULT OnInsertGroup(UINT, WPARAM wp, LPARAM lp) {
+			LVGROUP* arg = (LVGROUP*)lp;
+			if ((arg->mask & (LVGF_GROUPID | LVGF_HEADER)) != (LVGF_GROUPID | LVGF_HEADER)) return -1;
+
+			auto LVGroupID = arg->iGroupId;
+			for (auto& existing : m_groups) {
+				if (existing.LVGroupID == LVGroupID) return -1;
+			}
+
+			size_t idx = (size_t)wp;
+			if (idx > m_groups.size()) idx = m_groups.size();
+
+			group_t grp;
+			grp.LVGroupID = LVGroupID;
+			grp.GroupID = this->groupIDGen();
+			grp.header = pfc::utf8FromWide(arg->pszHeader);
+			m_groups.insert(m_groups.begin() + idx, std::move(grp));
+			// No reload data - adding of items does it
+			return (LRESULT)idx;
+		}
+		LRESULT OnRemoveGroup(UINT, WPARAM wp, LPARAM) {
+			int LVGroupID = (int)wp;
+			for (size_t walk = 0; walk < m_groups.size(); ++walk) {
+				if (m_groups[walk].LVGroupID == LVGroupID) {
+					m_groups.erase(m_groups.begin() + walk);
+					return walk;
+				}
+			}
+			return -1;
+		}
+		void getGroupInfo(group_t const& in, LVGROUP* out) {
+			if (out->mask & LVGF_HEADER) {
+				auto text = pfc::wideFromUTF8(in.header.c_str());
+				safeFillText(out->pszHeader, out->cchHeader, text);
+			}
+			if (out->mask & LVGF_GROUPID) {
+				out->iGroupId = in.LVGroupID;
+			}
+		}
+		void setGroupInfo(group_t& grp, LVGROUP* in) {
+			bool bReload = false;
+			if (in->mask & LVGF_HEADER) {
+				grp.header = pfc::utf8FromWide(in->pszHeader);
+				bReload = true;
+			}
+			if (in->mask & LVGF_GROUPID) {
+				grp.LVGroupID = in->iGroupId;
+				bReload = true;
+			}
+
+			if (bReload) this->ReloadData();
+		}
+		LRESULT OnGetGroupCount(UINT, WPARAM, LPARAM) {
+			return (LRESULT)m_groups.size();
+		}
+		LRESULT OnGetGroupInfo(UINT, WPARAM wp, LPARAM lp) {
+			int LVGroupID = (int)wp;
+			auto out = (LVGROUP*)lp;
+			for (auto& grp : m_groups) {
+				if (grp.LVGroupID == LVGroupID) {
+					getGroupInfo(grp, out);
+					return LVGroupID;
+				}
+			}
+			return -1;
+		}
+		LRESULT OnSetGroupInfo(UINT, WPARAM wp, LPARAM lp) {
+			int LVGroupID = (int)wp;
+			auto in = (LVGROUP*)lp;
+			int ret = -1;
+			for (auto& grp : m_groups) {
+				if (grp.LVGroupID == LVGroupID) {
+					setGroupInfo(grp, in);
+					ret = grp.LVGroupID;
+				}
+			}
+			return ret;
+		}
+		LRESULT OnGetGroupInfoByIndex(UINT, WPARAM wp, LPARAM lp) {
+			size_t idx = (size_t)wp;
+			LVGROUP* out = (LVGROUP*)lp;
+			if (idx < m_groups.size()) {
+				getGroupInfo(m_groups[idx], out);
+			}
+			return FALSE;
+		}
+		LRESULT OnRemoveAllGroups(UINT, WPARAM, LPARAM) {
+			m_groups.clear(); return 0;
+		}
+		LRESULT OnDeleteItem(UINT, WPARAM wp, LPARAM) {
+			size_t idx = (size_t)wp;
+			LRESULT rv = FALSE;
+			const size_t oldCount = m_content.size();
+			if (idx < oldCount) {
+				m_content.erase(m_content.begin() + idx);
+				this->OnItemsRemoved(pfc::bit_array_one(idx), oldCount);
+				rv = TRUE;
+			}
+			return rv;
+		}
+		LRESULT OnDeleteAllItems(UINT, WPARAM, LPARAM) {
+			m_content.clear(); ReloadData();
+			return TRUE;
+		}
+		int fillText(size_t item, size_t subItem, LVITEM* pItem) const {
+			pfc::wstringLite text;
+			if (item < m_content.size()) {
+				auto& r = m_content[item];
+				if (subItem < r.text.size()) {
+					auto& t = r.text[subItem];
+					if (!t.callback) {
+						text = pfc::wideFromUTF8(t.text);
+					}
+				}
+			}
+			return safeFillText(pItem->pszText, pItem->cchTextMax, text);
+		}
+		LRESULT OnGetItem(UINT, WPARAM, LPARAM lp) {
+			auto pItem = reinterpret_cast<LVITEM*>(lp);
+			if (pItem->mask & LVIF_TEXT) {
+				fillText(pItem->iItem, pItem->iSubItem, pItem);
+			}
+			if (pItem->mask & LVIF_IMAGE) {
+				size_t idx = pItem->iItem;
+				if (idx < m_content.size()) {
+					auto& line = m_content[idx];
+					pItem->iImage = line.image;
+				}
+			}
+			if (pItem->mask & LVIF_PARAM) {
+				pItem->lParam = GetItemParam(pItem->iItem);
+			}
+			
+			return TRUE;
+		}
+		LRESULT OnGetItemText(UINT, WPARAM wp, LPARAM lp) {
+			auto item = (size_t)wp;
+			auto pItem = reinterpret_cast<LVITEM*>(lp);
+			size_t subItem = (size_t)pItem->iSubItem;
+			return fillText(item, subItem, pItem);
+		}
+
+		bool GetCellCheckState(size_t item, size_t sub) const override {
+			bool rv = false;
+			if (sub == 0 && item < m_content.size()) {
+				rv = m_content[item].checked;
+			}
+			return rv;
+		}
+		void SetCellCheckState(size_t item, size_t sub, bool value) override {
+			if (sub == 0 && item < m_content.size()) {
+				auto& rec = m_content[item];
+				if (rec.checked != value) {
+					auto oldState = this->GetItemState(item);
+					rec.checked = value;
+					__super::SetCellCheckState(item, sub, value);
+
+					NMLISTVIEW info = { this->setupHdr(LVN_ITEMCHANGED) };
+					info.iItem = (int)item;
+					info.lParam = this->GetItemParam(item);
+					info.uChanged = LVIF_STATE;
+					info.uOldState = oldState;
+					info.uNewState = this->GetItemState(item);
+					this->sendNotify(&info);
+				}
+			}
+		}
+		UINT GetItemState(size_t idx) const override {
+			UINT ret = __super::GetItemState(idx);
+			if (useCheckBoxes() && idx < m_content.size()) {
+				int nCheck = m_content[idx].checked ? 2 : 1;
+				ret |= INDEXTOSTATEIMAGEMASK(nCheck);
+			}
+			return ret;
+		}
+		void SetItemState(size_t idx, DWORD mask, DWORD state) override {
+			__super::SetItemState(idx, mask, state);
+			if (useCheckBoxes() && (mask & LVIS_STATEIMAGEMASK) != 0) {
+				int nCheck = ((state & LVIS_STATEIMAGEMASK) >> 12);
+				this->SetCellCheckState(idx, 0, nCheck == 2);
+			}
+		}
+		groupID_t GetItemGroup(t_size p_item) const override {
+			if (m_groupViewEnabled && p_item < m_content.size()) {
+				return m_content[p_item].groupID;
+			}
+			return 0;
+		}
+		bool GetGroupHeaderText2(size_t baseItem, pfc::string_base& out) const override {
+			auto id = GetItemGroup(baseItem);
+			if (id != 0) {
+				for (auto& g : m_groups) {
+					if (id == g.GroupID) {
+						out = g.header;
+						return true;
+					}
+				}
+			}
+			return false;
+		}
+	};
+}
+
+HWND CListControl_ReplaceListView(HWND wndReplace) {
+	CListViewCtrl src(wndReplace);
+	const auto style = src.GetStyle();
+	HWND ret = NULL;
+	if (style & LVS_REPORT) {
+		const auto ctrlID = src.GetDlgCtrlID();
+		CWindow parent = src.GetParent();
+		DWORD headerStyle = 0;
+		if ((style & LVS_NOCOLUMNHEADER) == 0) {
+			auto header = src.GetHeader(); 
+			if (header) { 
+				headerStyle = header.GetStyle(); 
+			}
+		}
+		if (style & LVS_OWNERDATA) {
+			auto obj = PP::newWindowObj<CListControl_ListViewOwnerData>(style);
+			ret = obj->CreateInDialog(parent, ctrlID, src);
+			PFC_ASSERT(ret != NULL);
+			if (headerStyle != 0 && obj->GetHeaderCtrl() == NULL) {
+				obj->InitializeHeaderCtrl((headerStyle&(HDS_FULLDRAG | HDS_BUTTONS)));
+			}
+		} else {
+			PFC_ASSERT(src.GetItemCount() == 0); // transferring of items not yet implemented
+			auto obj = PP::newWindowObj<CListControl_ListView>(style);
+			ret = obj->CreateInDialog(parent, ctrlID, src);
+			PFC_ASSERT(ret != NULL);
+			if (headerStyle != 0 && obj->GetHeaderCtrl() == NULL) {
+				obj->InitializeHeaderCtrl((headerStyle & (HDS_FULLDRAG | HDS_BUTTONS)));
+			}
+		}
+	}
+	return ret;
+}
+
+namespace {
+	// FIX ME WM_DELETEITEM
+
+	class CListControl_ListBoxBase : public CListControlReadOnly {
+	protected:
+		const DWORD m_style;
+	public:
+		CListControl_ListBoxBase(DWORD style) : m_style(style) {
+
+			// owner data not implemented
+			PFC_ASSERT((m_style & LBS_NODATA) == 0);
+
+			if (m_style & LBS_NOSEL) {
+				this->SetSelectionModeNone();
+			} else if (m_style & LBS_MULTIPLESEL) {
+				this->SetSelectionModeMulti();
+			} else {
+				this->SetSelectionModeSingle();
+			}
+		}
+
+		BEGIN_MSG_MAP_EX(CListControl_ListBoxBase)
+			MSG_WM_SETFOCUS(OnSetFocus)
+			MSG_WM_KILLFOCUS(OnKillFocus)
+			MESSAGE_HANDLER_EX(LB_SETCURSEL, OnSetCurSel)
+			MESSAGE_HANDLER_EX(LB_GETCURSEL, OnGetCurSel)
+			MESSAGE_HANDLER_EX(LB_GETITEMRECT, OnGetItemRect)
+			MESSAGE_HANDLER_EX(LB_SELECTSTRING, OnSelectString)
+			MESSAGE_HANDLER_EX(LB_ITEMFROMPOINT, OnItemFromPoint)
+			MESSAGE_HANDLER_EX(LB_GETSEL, OnGetSel)
+			MESSAGE_HANDLER_EX(LB_SETSEL, OnSetSel)
+			MESSAGE_HANDLER_EX(LB_GETSELCOUNT, OnGetSelCount)
+			MESSAGE_HANDLER_EX(LB_GETSELITEMS, OnGetSelItems)
+			MESSAGE_HANDLER_EX(LB_SETCARETINDEX, OnSetCaretIndex)
+			MESSAGE_HANDLER_EX(LB_GETCARETINDEX, OnGetCaretIndex)
+			MSG_WM_CREATE(OnCreate)
+			CHAIN_MSG_MAP(CListControlReadOnly)
+		END_MSG_MAP()
+
+		LRESULT OnCreate(LPCREATESTRUCT) {
+			SetMsgHandled(FALSE);
+			// Adopt style flags of the original control to keep various ATL checks happy
+			DWORD style = GetStyle();
+			style = (style & 0xFFFF0000) | (m_style & 0xFFFF);
+			SetWindowLong(GWL_STYLE, style);
+			return 0;
+		}
+		void notifyParent(WORD code) {
+			if (m_style & LBS_NOTIFY) {
+				GetParent().PostMessage(WM_COMMAND, MAKEWPARAM(GetDlgCtrlID(), code), (LPARAM)m_hWnd);
+			}
+		}
+		LRESULT OnGetCurSel(UINT, WPARAM, LPARAM) {
+			return (LRESULT)this->GetSingleSel();
+		}
+		LRESULT OnSetCurSel(UINT, WPARAM wp, LPARAM) {
+			this->SelectSingle((size_t)wp);
+			return LB_OKAY;
+		}
+		LRESULT OnGetItemRect(UINT, WPARAM wp, LPARAM lp) {
+			size_t idx = (size_t)wp;
+			if (idx < this->GetItemCount()) {
+				*reinterpret_cast<RECT*>(lp) = this->GetItemRect(idx);
+				return LB_OKAY;
+			} else {
+				return LB_ERR;
+			}
+		}
+		LRESULT OnSelectString(UINT, WPARAM wp, LPARAM lp) {
+			auto idx = this->SendMessage(LB_FINDSTRING, wp, lp);
+			if (idx != LB_ERR) this->SelectSingle((size_t)idx);
+			return idx;
+		}
+		LRESULT OnItemFromPoint(UINT, WPARAM, LPARAM lp) {
+			CPoint pt(lp);
+			size_t ret;
+			if (!this->ItemFromPoint(pt, ret)) return LB_ERR;
+			return (LRESULT)ret;
+		}
+		LRESULT OnGetSel(UINT, WPARAM wp, LPARAM) {
+			size_t idx = (size_t)wp;
+			if (idx < this->GetItemCount()) {
+				return this->IsItemSelected(idx) ? 1 : 0;
+			} else {
+				return LB_ERR;
+			}
+
+		}
+		LRESULT OnSetSel(UINT, WPARAM wp, LPARAM lp) {
+			// Contrary to the other methods, index comes in LPARAM
+			if (lp < 0) {
+				if (wp) this->SelectAll();
+				else this->SelectNone();
+			} else {
+				this->SetSelection(pfc::bit_array_one((size_t)lp), pfc::bit_array_val(wp != 0));
+			}
+			return LB_OKAY;
+		}
+		LRESULT OnGetSelItems(UINT, WPARAM wp, LPARAM lp) {
+			int* out = reinterpret_cast<int*>(lp);
+			size_t outSize = (size_t)wp;
+			size_t outWalk = 0;
+			const size_t total = this->GetItemCount();
+			for (size_t walk = 0; walk < total && outWalk < outSize; ++walk) {
+				if (this->IsItemSelected(walk)) {
+					out[outWalk++] = (int)walk;
+				}
+			}
+			return outWalk;
+		}
+		LRESULT OnGetSelCount(UINT, WPARAM, LPARAM) {
+			return (LRESULT)this->GetSelectedCount();
+		}
+		LRESULT OnGetCaretIndex(UINT, WPARAM, LPARAM) {
+			size_t focus = this->GetFocusItem();
+			if (focus == SIZE_MAX) return LB_ERR;
+			return (LRESULT)focus;
+		}
+		LRESULT OnSetCaretIndex(UINT, WPARAM wp, LPARAM) {
+			size_t idx = (size_t)wp;
+			if (idx < this->GetItemCount()) {
+				this->SetFocusItem(idx); return LB_OKAY;
+			} else {
+				return LB_ERR;
+			}
+		}
+
+		void OnSetFocus(HWND) {
+			notifyParent(LBN_SETFOCUS);
+			SetMsgHandled(FALSE);
+		}
+		void OnKillFocus(HWND) {
+			notifyParent(LBN_KILLFOCUS);
+			SetMsgHandled(FALSE);
+		}
+
+		void OnSelectionChanged(pfc::bit_array const& affected, pfc::bit_array const& status) override {
+			__super::OnSelectionChanged(affected, status);
+			notifyParent(LBN_SELCHANGE);
+		}
+		void ExecuteDefaultAction(size_t idx) override {
+			notifyParent(LBN_DBLCLK);
+		}
+
+		void RequestReorder(size_t const* order, size_t count) override {}
+		void RequestRemoveSelection() override {}
+	};
+	class CListControl_ListBox : public CListControl_ListBoxBase {
+	public:
+		CListControl_ListBox(DWORD style) : CListControl_ListBoxBase(style) {}
+
+		BEGIN_MSG_MAP_EX(CListControl_ListBox)
+			MESSAGE_HANDLER_EX(LB_INSERTSTRING, OnInsertString)
+			MESSAGE_HANDLER_EX(LB_ADDSTRING, OnAddString)
+			MESSAGE_HANDLER_EX(LB_RESETCONTENT, OnResetContent)
+			MESSAGE_HANDLER_EX(LB_DELETESTRING, OnDeleteString)
+			MESSAGE_HANDLER_EX(LB_SETITEMDATA, OnSetItemData)
+			MESSAGE_HANDLER_EX(LB_GETITEMDATA, OnGetItemData)
+			MESSAGE_HANDLER_EX(LB_GETCOUNT, OnGetCount)
+			MESSAGE_HANDLER_EX(LB_FINDSTRING, OnFindString)
+			MESSAGE_HANDLER_EX(LB_FINDSTRINGEXACT, OnFindStringExact)
+			MESSAGE_HANDLER_EX(LB_SETCOUNT, OnSetCount)
+			MESSAGE_HANDLER_EX(LB_INITSTORAGE, OnInitStorage)
+			MESSAGE_HANDLER_EX(LB_GETTEXT, OnGetText)
+			MESSAGE_HANDLER_EX(LB_GETTEXTLEN, OnGetTextLen)
+			CHAIN_MSG_MAP(CListControl_ListBoxBase)
+		END_MSG_MAP()
+
+		struct rec_t {
+			pfc::string8 text;
+			LPARAM data = 0;
+		};
+
+		std::vector<rec_t> m_content;
+		t_size GetItemCount() const override {
+			return m_content.size();
+		}
+
+		bool isForceSorted() const {
+			return (m_style & LBS_SORT) != 0;
+		}
+		size_t AddStringSorted(pfc::string8 && str) {
+			size_t ret = 0;
+			// FIX ME bsearch??
+			// even with bsearch it's still O(n) so it's pointless
+			while (ret < m_content.size() && pfc::stricmp_ascii(str, m_content[ret].text) > 0) ++ret;
+
+			m_content.insert(m_content.begin() + ret, { std::move(str) });
+
+			this->OnItemsInserted(ret, 1, false);
+			return ret;
+		}
+		static pfc::string8 importString(LPARAM lp) {
+			return pfc::utf8FromWide(reinterpret_cast<const wchar_t*>(lp));
+		}
+		LRESULT OnInsertString(UINT, WPARAM wp, LPARAM lp) {
+			auto str = importString(lp);
+			if (isForceSorted()) return AddStringSorted(std::move(str));
+			size_t at = wp;
+			if (at > m_content.size()) at = m_content.size();
+			m_content.insert(m_content.begin() + at, { std::move(str) });
+			this->OnItemsInserted(at, 1, false);
+			return at;
+		}
+		LRESULT OnAddString(UINT, WPARAM wp, LPARAM lp) {
+			auto str = importString(lp);
+			if (isForceSorted()) return AddStringSorted(std::move(str));
+			size_t ret = m_content.size();
+			m_content.push_back({ std::move(str) });
+			this->OnItemsInserted(ret, 1, false);
+			return ret;
+		}
+		LRESULT OnResetContent(UINT, WPARAM, LPARAM) {
+			size_t oldCount = m_content.size();
+			if (oldCount > 0) {
+				m_content.clear();
+				this->OnItemsRemoved(pfc::bit_array_true(), oldCount);
+			}
+			return LB_OKAY;
+		}
+		LRESULT OnDeleteString(UINT, WPARAM wp, LPARAM) {
+			size_t idx = (size_t) wp;
+			if (idx < m_content.size()) {
+				m_content.erase(m_content.begin() + idx);
+				this->OnItemRemoved(idx);
+				return m_content.size();
+			} else {
+				return LB_ERR;
+			}
+		}
+
+		LRESULT OnSetItemData(UINT, WPARAM wp, LPARAM lp) {
+			size_t idx = (size_t)wp;
+			if (idx < m_content.size()) {
+				m_content[idx].data = (size_t)lp;
+				return LB_OKAY;
+			} else {
+				return LB_ERR;
+			}
+		}
+		LRESULT OnGetItemData(UINT, WPARAM wp, LPARAM lp) {
+			size_t idx = (size_t)wp;
+			if (idx < m_content.size()) {
+				return (LRESULT)m_content[idx].data;
+			} else {
+				return LB_ERR;
+			}
+		}
+		LRESULT OnGetCount(UINT, WPARAM, LPARAM) {
+			return (LRESULT)m_content.size();
+		}
+		LRESULT OnFindString(UINT, WPARAM wp, LPARAM lp) {
+			auto str = importString(lp);
+			const auto total = m_content.size();
+			size_t first = (size_t)(wp + 1) % total;
+			for (size_t walk = 0; walk < total; ++walk) {
+				size_t at = (first + walk) % total;
+				if (pfc::string_has_prefix_i(m_content[at].text, str)) return (LRESULT)at;
+			}
+			return LB_ERR;
+		}
+		LRESULT OnFindStringExact(UINT, WPARAM wp, LPARAM lp) {
+			auto str = importString(lp);
+			const auto total = m_content.size();
+			size_t first = (size_t)(wp + 1) % total;
+			for (size_t walk = 0; walk < total; ++walk) {
+				size_t at = (first + walk) % total;
+				if (pfc::stringEqualsI_utf8(m_content[at].text, str)) return (LRESULT)at;
+			}
+			return LB_ERR;
+		}
+
+		LRESULT OnSetCount(UINT, WPARAM wp, LPARAM) {
+			m_content.resize((size_t)wp);
+			ReloadData();
+			return LB_OKAY;
+		}
+		LRESULT OnInitStorage(UINT, WPARAM wp, LPARAM) {
+			m_content.reserve(m_content.size() + (size_t)wp);
+			return LB_OKAY;
+		}
+
+		LRESULT OnGetText(UINT, WPARAM wp, LPARAM lp) {
+			size_t idx = (size_t)wp;
+			if (idx < m_content.size()) {
+				auto out = reinterpret_cast<wchar_t*>(lp);
+				wcscpy(out, pfc::wideFromUTF8(m_content[idx].text.c_str()).c_str());
+				return LB_OKAY;
+			} else {
+				return LB_ERR;
+			}
+		}
+		LRESULT OnGetTextLen(UINT, WPARAM wp, LPARAM) {
+			size_t idx = (size_t)wp;
+			if (idx < m_content.size()) {
+				return pfc::wideFromUTF8(m_content[idx].text.c_str()).length();
+			} else {
+				return LB_ERR;
+			}
+		}
+
+		bool GetSubItemText(size_t item, size_t subItem, pfc::string_base& out) const override {
+			PFC_ASSERT(subItem == 0); (void)subItem;
+			PFC_ASSERT(item < m_content.size());
+			out = m_content[item].text;
+			return true;
+		}
+	};
+}
+
+HWND CListControl_ReplaceListBox(HWND wndReplace) {
+	CListBox src(wndReplace);
+	const auto style = src.GetStyle();
+	HWND ret = NULL;
+	if ((style & LBS_NODATA) == 0) {
+		PFC_ASSERT(src.GetCount() == 0); // transferring of items not yet implemented
+		const auto ctrlID = src.GetDlgCtrlID();
+		CWindow parent = src.GetParent();
+		auto obj = PP::newWindowObj<CListControl_ListBox>(style);
+		ret = obj->CreateInDialog(parent, ctrlID, src);
+	}
+	return ret;
+}