Mercurial > foo_out_sdl
diff foosdk/sdk/libPPUI/CListControl.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.cpp Mon Jan 05 02:15:46 2026 -0500 @@ -0,0 +1,1513 @@ +#include "stdafx.h" +#include "CListControl.h" +#include "PaintUtils.h" +#include "CListControlUserOptions.h" +#include "GDIUtils.h" +#include "DarkMode.h" + +#define PrepLayoutCache_Debug 0 +#define Scroll_Debug 0 + +#if Scroll_Debug +#define Scroll_Debug_Print(...) PFC_DEBUG_PRINT_FORCED(__VA_ARGS__) +#else +#define Scroll_Debug_Print(...) +#endif + +CListControlUserOptions * CListControlUserOptions::instance = nullptr; + +CRect CListControlImpl::GetClientRectHook() const { + CRect temp; + if ( m_hWnd == NULL || !GetClientRect(temp)) temp.SetRectEmpty(); + return temp; +} + +bool CListControlImpl::UserEnabledSmoothScroll() const { + auto i = CListControlUserOptions::instance; + if ( i != nullptr ) return i->useSmoothScroll(); + return false; +} + +LRESULT CListControlImpl::SetFocusPassThru(UINT,WPARAM,LPARAM,BOOL& bHandled) { + SetFocus(); + bHandled = FALSE; + return 0; +} + +void CListControlImpl::EnsureVisibleRectAbs(const CRect & p_rect) { + const CRect rcView = GetVisibleRectAbs(); + const CRect rcItem = p_rect; + int deltaX = 0, deltaY = 0; + + const bool centerOnItem = m_ensureVisibleUser; + + if (rcItem.top < rcView.top || rcItem.bottom > rcView.bottom) { + if (rcItem.Height() > rcView.Height()) { + deltaY = rcItem.top - rcView.top; + } else { + if (centerOnItem) { + deltaY = rcItem.CenterPoint().y - rcView.CenterPoint().y; + } else { + if (rcItem.bottom > rcView.bottom) deltaY = rcItem.bottom - rcView.bottom; + else deltaY = rcItem.top - rcView.top; + + } + } + } + if (rcItem.left < rcView.left || rcItem.right > rcView.right) { + if (rcItem.Width() > rcView.Width()) { + if (rcItem.left > rcView.left || rcItem.right < rcView.right) deltaX = rcItem.left - rcView.left; + } else { + if (centerOnItem) { + deltaX = rcItem.CenterPoint().x - rcView.CenterPoint().x; + } else { + if (rcItem.right > rcView.right) deltaX = rcItem.right - rcView.right; + else deltaX = rcItem.left - rcView.left; + } + } + } + + if (deltaX != 0 || deltaY != 0) { + MoveViewOriginDelta(CPoint(deltaX,deltaY)); + } +} +void CListControlImpl::EnsureItemVisible(t_size p_item, bool bUser) { + m_ensureVisibleUser = bUser; + PFC_ASSERT(p_item < GetItemCount()); + if (this->PrepLayoutCache(m_viewOrigin, p_item, p_item+1 )) { + RefreshSliders(); Invalidate(); + } + EnsureVisibleRectAbs(GetItemRectAbs(p_item)); + m_ensureVisibleUser = false; +} +void CListControlImpl::EnsureHeaderVisible2(size_t atItem) { + CRect rect; + if (GetGroupHeaderRectAbs2(atItem,rect)) EnsureVisibleRectAbs(rect); +} + +void CListControlImpl::RefreshSlider(bool p_vertical) { + const CRect viewArea = GetViewAreaRectAbs(); + const CRect rcVisible = GetVisibleRectAbs(); + SCROLLINFO si = {}; + si.cbSize = sizeof(si); + si.fMask = SIF_PAGE|SIF_RANGE|SIF_POS; + + + if (AllowScrollbar(p_vertical)) { + if (p_vertical) { + si.nPage = rcVisible.Height(); + si.nMin = viewArea.top; + si.nMax = viewArea.bottom - 1; + si.nPos = rcVisible.top; + } else { + si.nPage = rcVisible.Width(); + si.nMin = viewArea.left; + si.nMax = viewArea.right - 1; + si.nPos = rcVisible.left; + } + } + + Scroll_Debug_Print("RefreshSlider vertical=", p_vertical, ", nPage=", si.nPage, ", nMin=", si.nMin, ", nMax=", si.nMax, ", nPos=", si.nPos); + + SetScrollInfo(p_vertical ? SB_VERT : SB_HORZ, &si); +} + +void CListControlImpl::RefreshSliders() { + //PROBLEM: while lots of data can be reused across those, it has to be recalculated inbetween because view area etc may change when scroll info changes + RefreshSlider(false); RefreshSlider(true); +} + +int CListControlImpl::GetScrollThumbPos(int which) { + SCROLLINFO si = {}; + si.cbSize = sizeof(si); + si.fMask = SIF_TRACKPOS; + WIN32_OP_D( GetScrollInfo(which,&si) ); + return si.nTrackPos; +} + +bool CListControlImpl::ResolveGroupRangeCached(size_t itemInGroup, size_t& outBegin, size_t& outEnd) const { + auto end = this->m_groupHeaders.upper_bound(itemInGroup); + if (end == this->m_groupHeaders.begin()) return false; + auto begin = end; --begin; + outBegin = *begin; + if (end == this->m_groupHeaders.end()) outEnd = this->GetItemCount(); + else outEnd = *end; + return true; +} + +size_t CListControlImpl::ResolveGroupRange2(t_size p_base) const { + const auto id = this->GetItemGroup(p_base); + const size_t count = this->GetItemCount(); + size_t walk = p_base + 1; + while (walk < count && GetItemGroup(walk) == id) ++walk; + return walk - p_base; +} + + +static int HandleScroll(WORD p_code,int p_offset,int p_page, int p_line, int p_bottom, int p_thumbpos) { + switch(p_code) { + case SB_LINEUP: + return p_offset - p_line; + case SB_LINEDOWN: + return p_offset + p_line; + case SB_BOTTOM: + return p_bottom - p_page; + case SB_TOP: + return 0; + case SB_PAGEUP: + return p_offset - p_page; + case SB_PAGEDOWN: + return p_offset + p_page; + case SB_THUMBPOSITION: + return p_thumbpos; + case SB_THUMBTRACK: + return p_thumbpos; + default: + return p_offset; + } +} + +static CPoint ClipPointToRect(CPoint const & p_pt,CRect const & p_rect) { + return CPoint(pfc::clip_t(p_pt.x,p_rect.left,p_rect.right),pfc::clip_t(p_pt.y,p_rect.top,p_rect.bottom)); +} + +void CListControlImpl::MoveViewOriginNoClip(CPoint p_target) { + UpdateWindow(); + PrepLayoutCache(p_target); + const CPoint old = m_viewOrigin; + m_viewOrigin = p_target; + + if (m_viewOrigin != old) { +#if PrepLayoutCache_Debug + PFC_DEBUGLOG << "MoveViewOriginNoClip: m_viewOrigin=" << m_viewOrigin.x << "," << m_viewOrigin.y; +#endif + + if (m_viewOrigin.x != old.x) SetScrollPos(SB_HORZ,m_viewOrigin.x); + if (m_viewOrigin.y != old.y) SetScrollPos(SB_VERT,m_viewOrigin.y); + + const CPoint delta = old - m_viewOrigin; + if (FixedOverlayPresent()) Invalidate(); + else { + DWORD flags = SW_INVALIDATE | SW_ERASE; + const DWORD smoothScrollMS = 50; + if (this->UserEnabledSmoothScroll() && this->CanSmoothScroll()) { + flags |= SW_SMOOTHSCROLL | (smoothScrollMS << 16); + } + + ScrollWindowEx(delta.x,delta.y,GetClientRectHook(),NULL,0,0,flags ); + } + + OnViewOriginChange(m_viewOrigin - old); + } +} + +CPoint CListControlImpl::ClipViewOrigin(CPoint p_origin) const { + return ClipPointToRect(p_origin,GetValidViewOriginArea()); +} +void CListControlImpl::MoveViewOrigin(CPoint p_target) { + PrepLayoutCache(p_target); + MoveViewOriginNoClip(ClipViewOrigin(p_target)); +} + +#ifndef SPI_GETWHEELSCROLLCHARS +#define SPI_GETWHEELSCROLLCHARS 0x006C +#endif +int CListControlImpl::HandleWheel(int & p_accum,int p_delta, bool bHoriz) { + if ( m_suppressMouseWheel ) return 0; + UINT scrollLines = 1; + SystemParametersInfo(bHoriz ? SPI_GETWHEELSCROLLCHARS : SPI_GETWHEELSCROLLLINES,0,&scrollLines,0); + if (scrollLines == ~0) { + p_accum = 0; + int rv = -pfc::sgn_t(p_delta); + CRect client = GetClientRectHook(); + if (bHoriz) rv *= client.Width(); + else rv *= client.Height(); + return rv; + } + + const int itemHeight = GetItemHeight(); + const int extraScale = 10000; + + p_accum += p_delta * extraScale; + if ((int)scrollLines < 1) scrollLines = 1; + int multiplier = (WHEEL_DELTA * extraScale) / (scrollLines * itemHeight); + if (multiplier<1) multiplier = 1; + + int delta = pfc::rint32( (double) p_accum / (double) multiplier ); + p_accum -= delta * multiplier; + return -delta; + + /* + if (p_accum<=-multiplier || p_accum>=multiplier) { + int direction; + int ov = p_accum; + if (ov<0) { + direction = -1; + ov = -ov; + p_accum = - ((-p_accum)%multiplier); + } else { + p_accum %= multiplier; + direction = 1; + } + + return - (direction * (ov + multiplier - 1) ) / multiplier; + } else { + return 0; + } + */ +} + +LRESULT CListControlImpl::OnVWheel(UINT,WPARAM p_wp,LPARAM,BOOL&) { + const CRect client = GetClientRectHook(), view = this->GetViewAreaRectAbs(); + int deltaPixels = HandleWheel(m_wheelAccumY,(short)HIWORD(p_wp), false); + + const bool canVScroll = client.Height() < view.Height(); + const bool canHScroll = client.Width() < view.Width(); + + CPoint ptDelta; + if ( canVScroll && canHScroll && GetHotkeyModifierFlags() == MOD_SHIFT) { + ptDelta = CPoint(deltaPixels, 0); // default to horizontal scroll if shift is pressed + } else if (canVScroll) { + ptDelta = CPoint(0,deltaPixels); + } else if (canHScroll) { + ptDelta = CPoint(deltaPixels,0); + } + + if ( ptDelta != CPoint(0,0) ) { + MoveViewOriginDelta(ptDelta); + } + return 0; +} +LRESULT CListControlImpl::OnHWheel(UINT,WPARAM p_wp,LPARAM,BOOL&) { + // const CRect client = GetClientRectHook(); + int deltaPixels = HandleWheel(m_wheelAccumX,(short)HIWORD(p_wp), true); + MoveViewOriginDelta(CPoint(-deltaPixels,0)); + return 0; +} + +// WM_VSCROLL special fix +// We must expect SCROLLINFO to go out of sync with layout, due to group partitioning happening as the user scrolls +// SetScrollInfo() is apparently disregarded while the user is scrolling, causing nonsensical behavior if we live update it as we discover new groups +// When handling input, we must take the position as % of the set scrollbar range and map it to our coordinates - even though it is mappable directly if no groups etc are in use +LRESULT CListControlImpl::OnVScroll(UINT,WPARAM p_wp,LPARAM,BOOL&) { + SCROLLINFO si = {}; + si.cbSize = sizeof(si); + si.fMask = SIF_ALL; + WIN32_OP_D(GetScrollInfo(SB_VERT, &si)); + int thumb = si.nTrackPos; // HIWORD(p_wp); + auto bottom = GetViewAreaRectAbs().bottom; + auto visible = GetVisibleHeight(); + + if (si.nMax < si.nMin) return 0; + double p = (double)(thumb - si.nMin) / (double)(si.nMax + 1 - si.nMin); + thumb = pfc::rint32(p * bottom); + int target = HandleScroll(LOWORD(p_wp), m_viewOrigin.y, visible, GetItemHeight(), bottom, thumb); + + Scroll_Debug_Print("OnVScroll thumb=", thumb, ", target=", target, ", bottom=", bottom, ", visible=", visible, ", p=", p); + + MoveViewOrigin(CPoint(m_viewOrigin.x, target)); + + return 0; +} + +// ====== Logitech scroll bug explanation ====== +// With Logitech wheel hscroll, we must use WPARAM position, not GetScrollInfo() value. +// However this is wrong, we'll get nonsense if scroll range doesn't fit in 16-bit! +// As a workaround, we use GetScrollInfo() value for vscroll (good) +// and workaround Logitech bug by using WPARAM position with hscroll (practically impossible to overflow) +LRESULT CListControlImpl::OnHScroll(UINT,WPARAM p_wp,LPARAM,BOOL&) { + int thumb = HIWORD(p_wp); + const auto fullWidth = GetViewAreaWidth(); + if (fullWidth > INT16_MAX) { // Possible overflow or near-overflow? Drop Logitech stupidity mitigation + thumb = GetScrollThumbPos(SB_HORZ); + } + int target = HandleScroll(LOWORD(p_wp), m_viewOrigin.x, GetVisibleRectAbs().Width(), GetItemHeight() /*fixme*/, fullWidth, thumb); + Scroll_Debug_Print("OnHScroll thumb=", thumb, ", target=", target); + MoveViewOrigin(CPoint(target,m_viewOrigin.y)); + return 0; +} + +LRESULT CListControlImpl::OnGesture(UINT,WPARAM,LPARAM lParam,BOOL& bHandled) { + if (!this->m_gestureAPI.IsAvailable()) { + bHandled = FALSE; + return 0; + } + HGESTUREINFO hGesture = (HGESTUREINFO) lParam; + GESTUREINFO gestureInfo = {sizeof(gestureInfo)}; + if (m_gestureAPI.GetGestureInfo(hGesture, &gestureInfo)) { + //console::formatter() << "WM_GESTURE " << pfc::format_hex( gestureInfo.dwFlags ) << " " << (int)gestureInfo.dwID << " X:" << gestureInfo.ptsLocation.x << " Y:" << gestureInfo.ptsLocation.y << " arg:" << (__int64) gestureInfo.ullArguments; + CPoint pt( gestureInfo.ptsLocation.x, gestureInfo.ptsLocation.y ); + switch(gestureInfo.dwID) { + case GID_BEGIN: + m_gesturePoint = pt; + break; + case GID_END: + break; + case GID_PAN: + MoveViewOriginDelta( this->m_gesturePoint - pt); + m_gesturePoint = pt; + break; + } + } + + m_gestureAPI.CloseGestureInfoHandle(hGesture); + bHandled = TRUE; + return 0; +} + +LRESULT CListControlImpl::OnSize(UINT,WPARAM,LPARAM,BOOL&) { + this->PrepLayoutCache(m_viewOrigin); + OnSizeAsync_Trigger(); + RefreshSliders(); + return 0; +} + + +void CListControlImpl::RenderBackground( CDCHandle dc, CRect const & rc ) { + PaintUtils::FillRectSimple(dc,rc,GetSysColorHook(colorBackground)); +} + +void CListControlImpl::PaintContent(CRect rcPaint, HDC dc) { + CDCHandle renderDC(dc); + + CMemoryDC bufferDC(renderDC,rcPaint); + renderDC = bufferDC; + this->RenderBackground(renderDC, rcPaint); + + RenderRect(rcPaint, renderDC); +} + +void CListControlImpl::OnPrintClient(HDC dc, UINT) { + CRect rcClient; this->GetClientRect( rcClient ); + PaintContent( rcClient, dc ); +} + +void CListControlImpl::OnPaint(CDCHandle target) { + auto toggle = pfc::autoToggle(m_paintInProgress, true); + if (target) { + CRect rcClient; this->GetClientRect(rcClient); + PaintContent(rcClient, target); + } else { + CPaintDC paintDC(*this); + PaintContent(paintDC.m_ps.rcPaint, paintDC.m_hDC); + } +} + +bool CListControlImpl::GetItemRange(const CRect & p_rect,t_size & p_base,t_size & p_count) const { + return GetItemRangeAbs(this->RectClientToAbs(p_rect), p_base, p_count); +} + + + +bool CListControlImpl::GetItemRangeAbsInclHeaders(const CRect & p_rect,t_size & p_base,t_size & p_count) const { + CRect temp(p_rect); + temp.bottom += this->GetGroupHeaderHeight(); + return GetItemRangeAbs(temp, p_base, p_count); +} + +bool CListControlImpl::GetItemRangeAbs(const CRect & p_rect,t_size & p_base,t_size & p_count) const { + const size_t count = GetItemCount(); + if (p_rect.right < 0 || p_rect.left >= GetItemWidth() || count == 0) return false; + + size_t top = IndexFromPointAbs(CPoint(0, p_rect.top)); + size_t bottom = IndexFromPointAbs(CPoint(0, p_rect.bottom)); + if (top == SIZE_MAX) return false; + if (bottom > count-1) bottom = count - 1; + p_base = top; + p_count = bottom - top + 1; + PFC_ASSERT(p_base + p_count <= count); + return true; +} + +void CListControlImpl::RenderRect(const CRect & p_rect,CDCHandle p_dc) { + t_size base, count; + if (GetItemRange(p_rect,base,count)) { + for(t_size walk = 0; walk < count; ++walk) { + size_t atItem = base + walk; + if (m_groupHeaders.count(atItem) > 0) { + CRect rcHeader, rcUpdate; + if (GetGroupHeaderRectAbs2(atItem, rcHeader) ) { + rcHeader = RectAbsToClient(rcHeader); + if (rcUpdate.IntersectRect(rcHeader, p_rect)) { + DCStateScope dcState(p_dc); + if (p_dc.IntersectClipRect(rcUpdate) != NULLREGION) { + try { + RenderGroupHeader2(atItem, rcHeader, rcUpdate, p_dc); + } catch (...) { + PFC_ASSERT(!"Should not get here"); + } + } + } + } + } + + CRect rcUpdate, rcItem = GetItemRect(atItem); + if (rcUpdate.IntersectRect(rcItem,p_rect)) { + DCStateScope dcState(p_dc); + if (p_dc.IntersectClipRect(rcUpdate) != NULLREGION) { + try { + RenderItem(atItem,rcItem,rcUpdate,p_dc); + } catch(...) { + PFC_ASSERT(!"Should not get here"); + } + } + } + } + + if ( this->m_groupHeaders.size() > 0 ) { + auto iter = m_groupHeaders.upper_bound(base); + if (iter != m_groupHeaders.begin()) { + --iter; + while ( iter != m_groupHeaders.end() && *iter < base + count) { + auto iter2 = iter; ++iter2; + + size_t begin = *iter; + size_t end; + if (iter2 == m_groupHeaders.end()) end = this->GetItemCount(); + else end = *iter2; + + CRect rc; + rc.top = this->GetItemOffsetAbs(begin); + rc.bottom = this->GetItemBottomOffsetAbs(end-1); + rc.left = 0; + rc.right = this->GetItemWidth(); + rc = this->RectAbsToClient(rc); + CRect rcUpdate; + if (rcUpdate.IntersectRect(rc, p_rect)) { + DCStateScope dcState(p_dc); + if (p_dc.IntersectClipRect(rcUpdate) != NULLREGION) { + try { + this->RenderGroupOverlay(begin, rc, rcUpdate, p_dc); + } catch (...) { + PFC_ASSERT(!"Should not get here"); + } + } + } + + + iter = iter2; + } + } + } + } + + RenderOverlay2(p_rect,p_dc); +} + +bool CListControlImpl::GetGroupOverlayRectAbs(size_t atItem, CRect& outRect) { + auto iter = m_groupHeaders.upper_bound(atItem); + if (iter == m_groupHeaders.begin()) return false; + auto iter2 = iter; --iter; + + size_t begin = *iter; + size_t end; + if (iter2 == m_groupHeaders.end()) end = this->GetItemCount(); + else end = *iter2; + + CRect rc; + + rc.top = this->GetItemOffsetAbs(begin); + rc.bottom = this->GetItemBottomOffsetAbs(end - 1); + rc.left = 0; + rc.right = this->GetItemWidth(); + + outRect = rc; + return true; +} + +void CListControlImpl::MinGroupHeight2ChangedForGroup(groupID_t groupID, bool reloadWhole) { + for (auto iter = m_groupHeaders.begin(); iter != m_groupHeaders.end(); ++iter) { + if (groupID == GetItemGroup(*iter)) { + this->MinGroupHeight2Changed(*iter, reloadWhole); + } + } +} + +void CListControlImpl::UpdateGroupOverlayByID(groupID_t groupID, int xFrom, int xTo) { + t_size base, count; + if (GetItemRangeAbs(GetVisibleRectAbs(), base, count)) { + bool on = false; // Have to walk whole range - there may be multiple groups with the same ID + for (size_t walk = 0; walk < count; ++walk) { + bool test = (groupID == GetItemGroup(base + walk)); + if (test && !on) { + CRect rc; + if (GetGroupOverlayRectAbs(base + walk, rc)) { + if (xFrom < xTo) { + rc.left = xFrom; rc.right = xTo; + } + this->InvalidateRect(this->RectAbsToClient(rc)); + } + } + + on = test; + } + } +} + +CRect CListControlImpl::GetItemRect(t_size p_item) const { + return this->RectAbsToClient(GetItemRectAbs(p_item)); +} + +bool CListControlImpl::GetGroupHeaderRect2(size_t atItem,CRect & p_rect) const { + CRect temp; + if (!GetGroupHeaderRectAbs2(atItem,temp)) return false; + p_rect = RectAbsToClient(temp); + return true; +} + +size_t CListControlImpl::FindGroupBaseCached(size_t itemFor) const { + auto iter = m_groupHeaders.upper_bound(itemFor); + if (iter == m_groupHeaders.begin()) return 0; + --iter; + return *iter; +} + +size_t CListControlImpl::FindGroupBase(size_t itemFor) const { + return this->FindGroupBase(itemFor, this->GetItemGroup(itemFor)); +} + +size_t CListControlImpl::FindGroupBase(size_t itemFor, groupID_t id) const { + size_t walk = itemFor; + while (walk > 0) { + size_t prev = walk - 1; + if (this->GetItemGroup(prev) != id) break; + walk = prev; + } + return walk; +} + +bool CListControlImpl::PrepLayoutCache(CPoint& ptOrigin, size_t indexLo, size_t indexHi) { + const size_t count = GetItemCount(); + if (count == 0) return false; +#if PrepLayoutCache_Debug + PFC_DEBUGLOG << "PrepLayoutCache entry"; + PFC_DEBUGLOG << "PrepLayoutCache: count=" << count << " knownGroups=" << this->m_groupHeaders.size(); + PFC_DEBUGLOG << "PrepLayoutCache: indexLo=" << pfc::format_index(indexLo) << " indexHi=" << pfc::format_index(indexHi); +#endif + const int clientHeight = pfc::max_t<int>(this->GetClientRectHook().Height(), 100); + + // Always walk 2*clientHeight, with area above and below + int yMax = -1, yBase = 0; + size_t baseItem = 0, endItem = SIZE_MAX; + + if (!m_greedyGroupLayout) { + if (indexLo == SIZE_MAX) { + yBase = pfc::max_t<int>(ptOrigin.y - clientHeight / 2, 0); + yMax = yBase + clientHeight * 2; + baseItem = pfc::min_t<size_t>(this->IndexFromPointAbs(yBase), count - 1); + } else { + auto itemHeight = GetItemHeight(); + size_t extraItems = (size_t)(clientHeight / itemHeight); +#if PrepLayoutCache_Debug + PFC_DEBUGLOG << "PrepLayoutCache: clientHeight=" << clientHeight << " itemHeight=" << itemHeight << " extraItems=" << extraItems; +#endif + if (indexLo < extraItems) baseItem = 0; + else baseItem = indexLo - extraItems; + + if (indexHi == SIZE_MAX) { + endItem = baseItem + extraItems; + } else { + endItem = indexHi + extraItems; + } + if (endItem > count) endItem = count; + +#if PrepLayoutCache_Debug + PFC_DEBUGLOG << "PrepLayoutCache: baseItem=" << baseItem << " endItem=" << endItem; +#endif + } + } + + + + + size_t item = baseItem; + { + const auto group = this->GetItemGroup(baseItem); + if (group != 0) { + size_t hdr = this->FindGroupBase(baseItem, group); + if (hdr < baseItem) { + item = hdr; + } + } + } + +#if PrepLayoutCache_Debug + if (yMax != -1) { + PFC_DEBUGLOG << "PrepLayoutCache: yBase=" << yBase << " yMax=" << yMax; + } + if (indexLo != SIZE_MAX) { + pfc::string_formatter msg; + msg << "PrepLayoutCache: indexLo=" << indexLo; + if (indexHi != SIZE_MAX) { + msg << " indexHi=" << indexHi; + } + pfc::outputDebugLine(msg); + } + PFC_DEBUGLOG << "PrepLayoutCache: baseItem=" << baseItem; +#endif + + size_t anchorIdx = m_greedyGroupLayout ? SIZE_MAX : this->IndexFromPointAbs(ptOrigin.y); + int anchorDelta = 0; + bool anchorIsFirstInGroup = IsItemFirstInGroupCached(anchorIdx); + if (anchorIdx != SIZE_MAX) { + anchorDelta = ptOrigin.y - GetItemOffsetAbs(anchorIdx); + } + +#if PrepLayoutCache_Debug + PFC_DEBUGLOG << "PrepLayoutCache: anchorIdx=" << pfc::format_index(anchorIdx) << " anchorDelta=" << anchorDelta << " anchorIsFirstInGroup=" << anchorIsFirstInGroup; +#endif + + bool bChanged = false; + int gh = -1; + int ih = -1; + int yWalk = yBase; + groupID_t prevGroup = 0; + if (item > 1) prevGroup = this->GetItemGroup(item - 1); + for (; item < count; ++item) { + int yDelta = 0; + auto group = this->GetItemGroup(item); + if (group != prevGroup) { + if (m_groupHeaders.insert(item).second) bChanged = true; + if (gh < 0) gh = GetGroupHeaderHeight(); + yDelta += gh; + } else { + if (m_groupHeaders.erase(item) > 0) bChanged = true; + } + prevGroup = group; + + auto iter = m_varItemHeights.find(item); + int varHeight = this->GetItemHeight2(item); + if (varHeight < 0) { + if (iter != m_varItemHeights.end()) { + m_varItemHeights.erase(iter); + bChanged = true; + } + if (ih < 0) ih = this->GetItemHeight(); + yDelta += ih; + } else { + if (iter == m_varItemHeights.end()) { + m_varItemHeights[item] = varHeight; + bChanged = true; + } else if ( iter->second != varHeight ) { + iter->second = varHeight; + bChanged = true; + } + + yDelta += varHeight; + } + + if (item >= endItem) { + break; + } + if (item >= baseItem && yMax != -1) { + yWalk += yDelta; + if (yWalk > yMax) break; + } + } + +#if PrepLayoutCache_Debug + PFC_DEBUGLOG << "PrepLayoutCache: bChanged=" << bChanged << " knownGroups=" << m_groupHeaders.size() << " knownVarHeights=" << m_varItemHeights.size(); +#endif + + if (bChanged) { + if (anchorIdx != SIZE_MAX) { + int fix = GetItemOffsetAbs(anchorIdx) + anchorDelta; + + // View would begin exactly with an item that became a first item in a group? + if (anchorDelta == 0 && !anchorIsFirstInGroup && IsItemFirstInGroupCached(anchorIdx)) { + if (gh < 0) gh = GetGroupHeaderHeight(); + fix -= gh; + } + +#if PrepLayoutCache_Debug + PFC_DEBUGLOG << "PrepLayoutCache: fixing origin: " << ptOrigin.y << " to " << fix; +#endif + + ptOrigin.y = fix; + if (&ptOrigin != &m_viewOrigin && m_hWnd != NULL) { +#if PrepLayoutCache_Debug + PFC_DEBUGLOG << "PrepLayoutCache: invalidating view"; +#endif + Invalidate(); + } + } + } + + if ( bChanged ) { + // DO NOT update sliders from here, causes mayhem, SetScrollInfo() in mid-scroll is not really handled + // this->RefreshSliders(); + } + return bChanged; +} + +int CListControlImpl::GetViewAreaHeight() const { + auto ret = GetItemOffsetAbs(GetItemCount()); + Scroll_Debug_Print("GetViewAreaHeight: " , ret); + return ret; +} + +int CListControlImpl::GetItemBottomOffsetAbs(size_t item) const { + return GetItemOffsetAbs(item) + GetItemHeightCached(item); +} + +int CListControlImpl::GetItemOffsetAbs2(size_t base, size_t item) const { + // Also valid with item == GetItemCount() + size_t varcount = 0; + int acc = 0; + const bool baseValid = (base != SIZE_MAX); + const size_t itemDelta = baseValid ? item - base : item; + for (auto iter = (baseValid ? m_varItemHeights.lower_bound(base) : m_varItemHeights.begin()); iter != m_varItemHeights.end(); ++iter){ + if (iter->first >= item) break; + if (iter->second > 0) acc += iter->second; + ++varcount; + } + if (varcount < itemDelta) { + acc += GetItemHeight() * (int)(itemDelta - varcount); + } + + int gh = -1; + for (auto iter = (baseValid ? m_groupHeaders.upper_bound(base) : m_groupHeaders.begin()); iter != m_groupHeaders.end(); ++iter){ + if (*iter > item) break; + if (gh < 0) gh = GetGroupHeaderHeight(); + acc += gh; + } + + return acc; +} + +int CListControlImpl::GetItemOffsetAbs(size_t item) const { + // Also valid with item == GetItemCount() + return GetItemOffsetAbs2(SIZE_MAX, item); +} + +int CListControlImpl::GetItemContentHeightCached(size_t item) const { + auto iter = m_varItemHeights.find(item); + if (iter == m_varItemHeights.end()) return GetItemHeight(); + else return this->GetItemHeight2Content( item, iter->second ); +} + +int CListControlImpl::GetItemHeightCached(size_t item) const { + auto iter = m_varItemHeights.find(item); + if (iter == m_varItemHeights.end()) return GetItemHeight(); + else return iter->second; +} + +CRect CListControlImpl::GetItemRectAbs(t_size p_item) const { + PFC_ASSERT(p_item < GetItemCount()); + // const int normalHeight = GetItemHeight(); + CRect rcItem; + rcItem.top = GetItemOffsetAbs(p_item); + rcItem.bottom = rcItem.top + GetItemContentHeightCached(p_item); + rcItem.left = 0; + rcItem.right = rcItem.left + GetItemWidth(); + return rcItem; +} + +bool CListControlImpl::GetGroupHeaderRectAbs2(size_t atItem,CRect & p_rect) const { + + if (m_groupHeaders.count(atItem) == 0) return false; + + p_rect.bottom = GetItemOffsetAbs(atItem); + p_rect.top = p_rect.bottom - GetGroupHeaderHeight(); + p_rect.left = 0; + p_rect.right = GetItemWidth(); + return true; +} + +CRect CListControlImpl::GetViewAreaRectAbs() const { + return CRect(0,0,GetViewAreaWidth(),GetViewAreaHeight()); +} + +CRect CListControlImpl::GetViewAreaRect() const { + CRect rc = GetViewAreaRectAbs(); + rc.OffsetRect( - GetViewOffset() ); + CRect ret; ret.IntersectRect(rc,GetClientRectHook()); + return ret; +} + +void CListControlImpl::UpdateGroupHeader2(size_t atItem) { + CRect rect; + if (GetGroupHeaderRect2(atItem,rect)) { + InvalidateRect(rect); + } +} +static void AddUpdateRect(HRGN p_rgn,CRect const & p_rect) { + CRgn temp; temp.CreateRectRgnIndirect(p_rect); + CRgnHandle(p_rgn).CombineRgn(temp,RGN_OR); +} + +void CListControlImpl::OnItemsReordered( const size_t * order, size_t count ) { + PFC_ASSERT(count == GetItemCount()); (void)count; + ReloadItems( pfc::bit_array_order_changed(order) ); +} +void CListControlImpl::UpdateItems(const pfc::bit_array & p_mask) { + t_size base,count; + if (GetItemRangeAbs(GetVisibleRectAbs(),base,count)) { + const t_size max = base+count; + CRgn updateRgn; updateRgn.CreateRectRgn(0,0,0,0); + bool found = false; + for(t_size walk = p_mask.find_first(true,base,max); walk < max; walk = p_mask.find_next(true,walk,max)) { + found = true; + AddUpdateRect(updateRgn,GetItemRect(walk)); + } + if (found) { + InvalidateRgn(updateRgn); + } + } +} + +std::pair<size_t, size_t> CListControlImpl::GetVisibleRange() const { + const size_t total = GetItemCount(); + CRect rcVisible = this->GetVisibleRectAbs(); + size_t lo = this->IndexFromPointAbs(rcVisible.top); + PFC_ASSERT(lo != SIZE_MAX); + if (lo == SIZE_MAX) lo = 0; // should not happen + size_t hi = this->IndexFromPointAbs(rcVisible.bottom); + if (hi < total) ++hi; + else hi = total; + return { lo, hi }; +} + +bool CListControlImpl::IsItemVisible(size_t which) const { + CRect rcVisible = this->GetVisibleRectAbs(); + CRect rcItem = this->GetItemRectAbs(which); + return rcItem.top >= rcVisible.top && rcItem.bottom <= rcVisible.bottom; +} + +void CListControlImpl::UpdateItemsAndHeaders(const pfc::bit_array & p_mask) { + t_size base,count; + groupID_t groupWalk = 0; + if (GetItemRangeAbsInclHeaders(GetVisibleRectAbs(),base,count)) { + const t_size max = base+count; + CRgn updateRgn; updateRgn.CreateRectRgn(0,0,0,0); + bool found = false; + for(t_size walk = p_mask.find_first(true,base,max); walk < max; walk = p_mask.find_next(true,walk,max)) { + found = true; + const groupID_t groupId = GetItemGroup(walk); + if (groupId != groupWalk) { + CRect rect; + if (GetGroupHeaderRect2(walk,rect)) { + AddUpdateRect(updateRgn,rect); + } + groupWalk = groupId; + } + AddUpdateRect(updateRgn,GetItemRect(walk)); + } + if (found) { + InvalidateRgn(updateRgn); + } + } +} + + +CRect CListControlImpl::GetValidViewOriginArea() const { + const CRect rcView = GetViewAreaRectAbs(); + const CRect rcClient = GetClientRectHook(); + CRect rcArea = rcView; + rcArea.right -= pfc::min_t(rcView.Width(),rcClient.Width()); + rcArea.bottom -= pfc::min_t(rcView.Height(),rcClient.Height()); + return rcArea; +} + +void CListControlImpl::OnViewAreaChanged(CPoint p_originOverride) { + const CPoint oldViewOrigin = m_viewOrigin; + + PrepLayoutCache(p_originOverride); + + m_viewOrigin = ClipPointToRect(p_originOverride,GetValidViewOriginArea()); + + if (m_viewOrigin != p_originOverride) { + // Did clip from the requested? + PrepLayoutCache(m_viewOrigin); + } +#if PrepLayoutCache_Debug + PFC_DEBUGLOG << "OnViewAreaChanged: m_viewOrigin=" << m_viewOrigin.x << "," << m_viewOrigin.y; +#endif + + RefreshSliders(); + + Invalidate(); + + if (oldViewOrigin != m_viewOrigin) { + OnViewOriginChange(m_viewOrigin - oldViewOrigin); + } +} + +size_t CListControlImpl::IndexFromPointAbs(CPoint pt) const { + if (pt.x < 0 || pt.x >= GetItemWidth()) return SIZE_MAX; + return IndexFromPointAbs(pt.y); +} + +size_t CListControlImpl::IndexFromPointAbs(int ptY) const { + const size_t count = GetItemCount(); + if (count == 0) return SIZE_MAX; + + class wrapper { + public: + wrapper(const CListControlImpl & o) : owner(o) {} + int operator[] (size_t idx) const { + // Return LAST line of this item + return owner.GetItemBottomOffsetAbs(idx)-1; + } + const CListControlImpl & owner; + }; + + wrapper w(*this); + size_t result = SIZE_MAX; + pfc::binarySearch<>::run(w, 0, count, ptY, result); + PFC_ASSERT(result != SIZE_MAX); + return result; +} + +bool CListControlImpl::ItemFromPointAbs(CPoint const & p_pt,t_size & p_item) const { + size_t idx = IndexFromPointAbs(p_pt); + if (idx >= GetItemCount()) return false; + CRect rc = this->GetItemRectAbs(idx); + if (!rc.PtInRect(p_pt)) return false; + p_item = idx; + return true; +} + +size_t CListControlImpl::ItemFromPointAbs(CPoint const& p_pt) const { + size_t ret = SIZE_MAX; + ItemFromPointAbs(p_pt, ret); + return ret; +} + +bool CListControlImpl::GroupHeaderFromPointAbs2(CPoint const & p_pt,size_t & atItem) const { + size_t idx = IndexFromPointAbs(p_pt); + if (idx == SIZE_MAX) return false; + CRect rc; + if (!this->GetGroupHeaderRectAbs2(idx, rc)) return false; + if (!rc.PtInRect(p_pt)) return false; + atItem = idx; + return true; +} + +void CListControlImpl::OnThemeChanged() { + m_themeCache.remove_all(); +} + +CTheme & CListControlImpl::themeFor(const char * what) { + bool bNew; + auto & ret = this->m_themeCache.find_or_add_ex( what, bNew ); + if (bNew) ret.OpenThemeData(*this, pfc::stringcvt::string_wide_from_utf8(what)); + return ret; +} + +void CListControlImpl::SetDarkMode(bool v) { + if (m_darkMode != v) { + m_darkMode = v; + RefreshDarkMode(); + } +} + +void CListControlImpl::RefreshDarkMode() { + if (m_hWnd != NULL) { + Invalidate(); + + // GOD DAMNIT: Should use ItemsView, but only Explorer fixes scrollbars + DarkMode::ApplyDarkThemeCtrl(m_hWnd, m_darkMode, L"Explorer"); + } +} + +LRESULT CListControlImpl::OnCreatePassThru(UINT,WPARAM,LPARAM,BOOL& bHandled) { + + RefreshDarkMode(); + + + OnViewAreaChanged(); + + if (m_gestureAPI.IsAvailable()) { + GESTURECONFIG config = {GID_PAN, GC_PAN_WITH_SINGLE_FINGER_VERTICALLY|GC_PAN_WITH_INERTIA, GC_PAN_WITH_SINGLE_FINGER_HORIZONTALLY | GC_PAN_WITH_GUTTER}; + m_gestureAPI.SetGestureConfig( *this, 0, 1, &config, sizeof(GESTURECONFIG)); + } + + bHandled = FALSE; + return 0; +} +bool CListControlImpl::IsSameItemOrHeaderAbs(const CPoint & p_point1, const CPoint & p_point2) const { + t_size item1, item2; + if (ItemFromPointAbs(p_point1, item1)) { + if (ItemFromPointAbs(p_point2,item2)) { + return item1 == item2; + } else { + return false; + } + } + if (GroupHeaderFromPointAbs2(p_point1, item1)) { + if (GroupHeaderFromPointAbs2(p_point2, item2)) { + return item1 == item2; + } else { + return false; + } + } + return false; +} + +void CListControlImpl::OnSizeAsync_Trigger() { + if (!m_sizeAsyncPending) { + if (PostMessage(MSG_SIZE_ASYNC,0,0)) { + m_sizeAsyncPending = true; + } else { + PFC_ASSERT(!"Shouldn't get here!"); + //should not happen + ListHandleResize(); + } + } +} + +void CListControlImpl::ListHandleResize() { + MoveViewOriginDelta(CPoint(0,0)); + m_sizeAsyncPending = false; +} + +void CListControlImpl::AddGroupHeaderToUpdateRgn2(HRGN p_rgn, size_t atItem) const { + CRect rcHeader; + if (GetGroupHeaderRect2(atItem,rcHeader)) AddUpdateRect(p_rgn,rcHeader); +} +void CListControlImpl::AddItemToUpdateRgn(HRGN p_rgn, t_size p_index) const { + if (p_index < this->GetItemCount()) { + AddUpdateRect(p_rgn,GetItemRect(p_index)); + } +} + +COLORREF CListControlImpl::GetSysColorHook(int colorIndex) const { + if (m_darkMode) { + return DarkMode::GetSysColor(colorIndex); + } else { + return GetSysColor(colorIndex); + } +} + +BOOL CListControlImpl::OnEraseBkgnd(CDCHandle dc) { + + if (paintInProgress()) return FALSE; + + CRect rcClient; WIN32_OP_D(GetClientRect(rcClient)); // SPECIAL CASE: No GetClientRectHook() here, fill physical client area, not logical + PaintUtils::FillRectSimple(dc,rcClient,this->GetSysColorHook(COLOR_WINDOW)); + + return TRUE; +} + +t_size CListControlImpl::InsertIndexFromPointEx(const CPoint & pt, bool & bInside) const { + bInside = false; + int y_abs = pt.y + GetViewOffset().y; + + if (y_abs >= GetViewAreaHeight()) { + return GetItemCount(); + } + size_t itemIdx = IndexFromPointAbs(y_abs); + if (itemIdx == SIZE_MAX) return SIZE_MAX; + + { + CRect rc; + if (!this->GetGroupHeaderRectAbs2(itemIdx, rc)) { + if (y_abs >= rc.top && y_abs <= rc.bottom) { + bInside = false; + return itemIdx; + } + } + } + if (itemIdx != SIZE_MAX) { + const CRect rc = GetItemRectAbs(itemIdx); + if (y_abs > rc.top + MulDiv(rc.Height(), 2, 3)) itemIdx++; + else if (y_abs >= rc.top + MulDiv(rc.Height(), 1, 3)) bInside = true; + return itemIdx; + } + return SIZE_MAX; +} + +t_size CListControlImpl::InsertIndexFromPoint(const CPoint & pt) const { + bool dummy; return InsertIndexFromPointEx(pt,dummy); +} + +COLORREF CListControlImpl::BlendGridColor( COLORREF bk ) { + return BlendGridColor( bk, PaintUtils::DetermineTextColor( bk ) ); +} + +COLORREF CListControlImpl::BlendGridColor( COLORREF bk, COLORREF tx ) { + return PaintUtils::BlendColor(bk, tx, 10); +} + +COLORREF CListControlImpl::GridColor() { + return BlendGridColor( GetSysColorHook(colorBackground), GetSysColorHook(colorText) ); +} + +void CListControlImpl::RenderItemBackground(CDCHandle p_dc,const CRect & p_itemRect,size_t p_item, uint32_t bkColor) { + switch( this->m_rowStyle ) { + case rowStylePlaylistDelimited: + PaintUtils::RenderItemBackground(p_dc,p_itemRect,p_item,bkColor); + { + auto blend = BlendGridColor(bkColor); + CDCPen pen(p_dc, blend); + SelectObjectScope scope(p_dc, pen); + + p_dc.MoveTo( p_itemRect.right-1, p_itemRect.top ); + p_dc.LineTo( p_itemRect.right-1, p_itemRect.bottom ); + } + break; + case rowStylePlaylist: + PaintUtils::RenderItemBackground(p_dc,p_itemRect,p_item,bkColor); + break; + case rowStyleGrid: + PaintUtils::FillRectSimple(p_dc, p_itemRect, bkColor ); + { + auto blend = BlendGridColor(bkColor); + CDCBrush brush(p_dc, blend); + p_dc.FrameRect(&p_itemRect, brush); + + } + break; + case rowStyleFlat: + PaintUtils::FillRectSimple(p_dc, p_itemRect, bkColor ); + break; + } +} + +void CListControlImpl::RenderGroupHeaderBackground(CDCHandle p_dc,const CRect & p_headerRect,int p_group) { + (void)p_group; + const t_uint32 bkColor = GetSysColorHook(colorBackground); + size_t pretendIndex = 0; + switch( this->m_rowStyle ) { + default: + PaintUtils::FillRectSimple( p_dc, p_headerRect, bkColor ); + break; + case rowStylePlaylistDelimited: + case rowStylePlaylist: + PaintUtils::RenderItemBackground(p_dc,p_headerRect,pretendIndex,bkColor); + break; + } +} + +void CListControlImpl::RenderItem(t_size p_item,const CRect & p_itemRect,const CRect & p_updateRect,CDCHandle p_dc) { + this->RenderItemBackground(p_dc, p_itemRect, p_item, GetSysColorHook(colorBackground) ); + + DCStateScope backup(p_dc); + p_dc.SetBkMode(TRANSPARENT); + p_dc.SetBkColor(GetSysColorHook(colorBackground)); + p_dc.SetTextColor(GetSysColorHook(colorText)); + + RenderItemText(p_item,p_itemRect,p_updateRect,p_dc, true); +} + +void CListControlImpl::RenderGroupHeader2(size_t baseItem,const CRect & p_headerRect,const CRect & p_updateRect,CDCHandle p_dc) { + this->RenderGroupHeaderBackground(p_dc, p_headerRect, 0 ); + + DCStateScope backup(p_dc); + p_dc.SetBkMode(TRANSPARENT); + p_dc.SetBkColor(GetSysColorHook(colorBackground)); + p_dc.SetTextColor(GetSysColorHook(colorHighlight)); + + RenderGroupHeaderText2(baseItem,p_headerRect,p_updateRect,p_dc); +} + + +CListControlFontOps::CListControlFontOps() : m_font((HFONT)::GetStockObject(DEFAULT_GUI_FONT)), m_itemHeight(), m_groupHeaderHeight() { + UpdateGroupHeaderFont(); + CalculateHeights(); +} + +void CListControlFontOps::UpdateGroupHeaderFont() { + try { + m_groupHeaderFont = NULL; + LOGFONT lf = {}; + WIN32_OP_D( m_font.GetLogFont(lf) ); + lf.lfHeight = pfc::rint32( (double) lf.lfHeight * GroupHeaderFontScale() ); + lf.lfWeight = GroupHeaderFontWeight(lf.lfWeight); + WIN32_OP_D( m_groupHeaderFont.CreateFontIndirect(&lf) != NULL ); + } catch(std::exception const & e) { + (void) e; + // console::print(e.what()); + m_groupHeaderFont = (HFONT)::GetStockObject(DEFAULT_GUI_FONT); + } +} + +void CListControlFontOps::CalculateHeights() { + const t_uint32 spacing = MulDiv(4, m_dpi.cy, 96); + m_itemHeight = GetFontHeight( m_font ) + spacing; + m_groupHeaderHeight = GetFontHeight( m_groupHeaderFont ) + spacing; +} + +void CListControlFontOps::SetFont(HFONT font,bool bUpdateView) { + m_font = font; + UpdateGroupHeaderFont(); CalculateHeights(); + OnSetFont(bUpdateView); + if (bUpdateView && m_hWnd != NULL) OnViewAreaChanged(); + +} + +LRESULT CListControlFontOps::OnSetFont(UINT,WPARAM wp,LPARAM,BOOL&) { + SetFont((HFONT)wp); + return 0; +} + +LRESULT CListControlFontOps::OnGetFont(UINT,WPARAM,LPARAM,BOOL&) { + return (LRESULT)(HFONT)m_font; +} + +LRESULT CListControlImpl::OnGetDlgCode(UINT, WPARAM wp, LPARAM) { + switch(wp) { + case VK_RETURN: + return m_dlgWantEnter ? DLGC_WANTMESSAGE : 0; + default: + SetMsgHandled(FALSE); + return 0; + } +} + +HWND CListControlImpl::CreateInDialog(CWindow wndDialog, UINT replaceControlID, CWindow lstReplace) { + PFC_ASSERT(lstReplace != NULL); + auto status = lstReplace.SendMessage(WM_GETDLGCODE, VK_RETURN); + m_dlgWantEnter = (status & DLGC_WANTMESSAGE); + CRect rc; + CWindow wndPrev = wndDialog.GetNextDlgTabItem(lstReplace, TRUE); + WIN32_OP_D(lstReplace.GetWindowRect(&rc)); + WIN32_OP_D(wndDialog.ScreenToClient(rc)); + WIN32_OP_D(lstReplace.DestroyWindow()); + WIN32_OP_D(this->Create(wndDialog, &rc, 0, 0, WS_EX_STATICEDGE, replaceControlID)); + if (wndPrev != NULL) this->SetWindowPos(wndPrev, 0, 0, 0, 0, SWP_NOSIZE | SWP_NOMOVE); + // this->BringWindowToTop(); + this->SetFont(wndDialog.GetFont()); + return m_hWnd; +} + +HWND CListControlImpl::CreateInDialog(CWindow wndDialog, UINT replaceControlID ) { + return this->CreateInDialog(wndDialog, replaceControlID, wndDialog.GetDlgItem(replaceControlID)); +} + + +void CListControlImpl::defer(std::function<void() > f) { + m_deferred.push_back( f ); + if (!m_defferredMsgPending) { + if ( PostMessage(MSG_EXEC_DEFERRED) ) m_defferredMsgPending = true; + } +} + +LRESULT CListControlImpl::OnExecDeferred(UINT, WPARAM, LPARAM) { + + for ( ;; ) { + auto i = m_deferred.begin(); + if ( i == m_deferred.end() ) break; + auto op = std::move(*i); + m_deferred.erase(i); // erase first, execute later - avoid erratic behavior if op alters the list + op(); + } + + m_defferredMsgPending = false; + return 0; +} + +// ======================================================================================== +// Mouse wheel vs drag&drop hacks +// Install MouseHookProc for the duration of DoDragDrop and handle the input from there +// ======================================================================================== +static HHOOK g_hook = NULL; +static CListControlImpl * g_dragDropInstance = nullptr; +LRESULT CALLBACK CListControlImpl::MouseHookProc(int nCode, WPARAM wParam, LPARAM lParam) { + if (nCode == HC_ACTION && g_dragDropInstance != nullptr) { + switch (wParam) { + case WM_MOUSEWHEEL: + case WM_MOUSEHWHEEL: + g_dragDropInstance->MouseWheelFromHook((UINT)wParam, lParam); + break; + } + } + return CallNextHookEx(g_hook, nCode, wParam, lParam); +} + +bool CListControlImpl::MouseWheelFromHook(UINT msg, LPARAM data) { + MOUSEHOOKSTRUCTEX const * mhs = reinterpret_cast<MOUSEHOOKSTRUCTEX const *> ( data ); + if ( ::WindowFromPoint(mhs->pt) != m_hWnd ) return false; + LRESULT dummyResult = 0; + WPARAM wp = mhs->mouseData; + LPARAM lp = MAKELPARAM( mhs->pt.x, mhs->pt.y ); + // If we get here, m_suppressMouseWheel should be true per our DoDragDrop() + pfc::vartoggle_t<bool> scope(m_suppressMouseWheel, false); + this->ProcessWindowMessage( m_hWnd, msg, wp, lp, dummyResult ); + return true; +} + +HRESULT CListControlImpl::DoDragDrop(LPDATAOBJECT pDataObj, LPDROPSOURCE pDropSource, DWORD dwOKEffects, LPDWORD pdwEffect) { + HRESULT ret = E_FAIL; + // Should not get here with non null g_dragDropInstance - means we have a recursive call + PFC_ASSERT(g_dragDropInstance == nullptr); + if ( g_dragDropInstance == nullptr ) { + // futureproofing: kill mouse wheel message processing if we get them delivered the regular way while this is in progress + pfc::vartoggle_t<bool> scope(m_suppressMouseWheel, true); + g_dragDropInstance = this; + g_hook = SetWindowsHookEx(WH_MOUSE, MouseHookProc, NULL, GetCurrentThreadId()); + try { + ret = ::DoDragDrop(pDataObj, pDropSource, dwOKEffects, pdwEffect); + } catch (...) { + } + g_dragDropInstance = nullptr; + UnhookWindowsHookEx(pfc::replace_null_t(g_hook)); + } + return ret; +} + + +CPoint CListControlImpl::PointAbsToClient(CPoint pt) const { + return pt - GetViewOffset(); +} + +CPoint CListControlImpl::PointClientToAbs(CPoint pt) const { + return pt + GetViewOffset(); +} + +CRect CListControlImpl::RectAbsToClient(CRect rc) const { + CRect ret; +#if 1 + ret = rc; + ret.OffsetRect(-GetViewOffset()); +#else + ret.TopLeft() = PointAbsToClient(rc.TopLeft()); + ret.BottomRight() = PointAbsToClient(rc.BottomRight()); +#endif + return ret; +} + +CRect CListControlImpl::RectClientToAbs(CRect rc) const { + CRect ret; +#if 1 + ret = rc; + ret.OffsetRect(GetViewOffset()); +#else + ret.TopLeft() = PointClientToAbs(rc.TopLeft()); + ret.BottomRight() = PointAbsToClient(rc.BottomRight()); +#endif + return ret; +} + +size_t CListControlImpl::ItemFromPoint(CPoint const& pt) const { + size_t ret = SIZE_MAX; + if (!ItemFromPoint(pt, ret)) ret = SIZE_MAX; + return ret; +} + +bool CListControlImpl::ItemFromPoint(CPoint const & p_pt, t_size & p_item) const { + return ItemFromPointAbs( PointClientToAbs( p_pt ), p_item); +} + +void CListControlImpl::ReloadData() { + this->m_varItemHeights.clear(); + this->m_groupHeaders.clear(); + OnViewAreaChanged(); +} + +void CListControlImpl::ReloadItems(pfc::bit_array const & mask) { + bool bReLayout = false; + mask.for_each(true, 0, GetItemCount(), [this, &bReLayout] (size_t idx) { + int hNew = this->GetItemHeight2(idx); + int hOld = -1; + { + auto iter = m_varItemHeights.find(idx); + if (iter != m_varItemHeights.end()) hOld = iter->second; + } + if (hNew != hOld) { + m_varItemHeights[idx] = hNew; + bReLayout = true; + } + }); + if (bReLayout) { + OnViewAreaChanged(); + } else { + UpdateItems(mask); + } + +} + +void CListControlImpl::MinGroupHeight2Changed(size_t itemInGroup, bool reloadWhole) { + size_t lo, hi; + if (ResolveGroupRangeCached(itemInGroup, lo, hi)) { + if (reloadWhole) { + CRect rc; + if (this->GetGroupOverlayRectAbs(itemInGroup, rc)) { + this->InvalidateRect(this->RectAbsToClient(rc)); + } + { + pfc::bit_array_range range(lo, hi-lo); + this->ReloadItems(range); + } + } else { + this->ReloadItem(hi - 1); + } + } +} + +bool CListControlImpl::IsItemFirstInGroupCached(size_t item) const { + return m_groupHeaders.count(item) > 0; +} + +bool CListControlImpl::IsItemFirstInGroup(size_t item) const { + if (item == 0) return true; + return GetItemGroup(item) != GetItemGroup(item - 1); +} +bool CListControlImpl::IsItemLastInGroup(size_t item) const { + size_t next = item + 1; + if (next >= GetItemCount()) return true; + return GetItemGroup(item) != GetItemGroup(next); +} + +int CListControlImpl::GetItemHeight2(size_t which) const { + if (!IsItemLastInGroup(which)) return -1; + + const int minGroupHeight = this->GetMinGroupHeight2(which); + if (minGroupHeight <= 0) return -1; + + const int heightNormal = this->GetItemHeight(); + + const size_t base = FindGroupBase(which); + + const int groupHeightWithout = (which > base ? this->GetItemOffsetAbs2(base,which) : 0); + + const int minItemHeight = minGroupHeight - groupHeightWithout; // possibly negative + + if (minItemHeight > heightNormal) return minItemHeight; + else return -1; // use normal +} + +void CListControlImpl::wndSetDarkMode(CWindow wndListControl, bool bDark) { + wndListControl.SendMessage(DarkMode::msgSetDarkMode(), bDark ? 1 : 0); +} + +LRESULT CListControlImpl::OnSetDark(UINT, WPARAM wp, LPARAM) { + switch (wp) { + case 0: + this->SetDarkMode(false); + break; + case 1: + this->SetDarkMode(true); + break; + } + return 1; +} + +void CListControlImpl::OnItemRemoved(size_t which) { + this->OnItemsRemoved(pfc::bit_array_one(which), GetItemCount() + 1); +} + + +UINT CListControlImpl::msgSetDarkMode() { + return DarkMode::msgSetDarkMode(); +} + +void CListControlImpl::OnKeyDown(UINT nChar, UINT nRepCnt, UINT nFlags) { + (void)nRepCnt; (void)nFlags; + switch (nChar) { + case VK_LEFT: + MoveViewOriginDelta(CPoint(-MulDiv(16, m_dpi.cx, 96), 0)); + break; + case VK_RIGHT: + MoveViewOriginDelta(CPoint(MulDiv(16, m_dpi.cx, 96), 0)); + break; + default: + SetMsgHandled(FALSE); break; + } +} + +void CListControlImpl::OnItemsInserted(size_t at, size_t count, bool bSelect) { + size_t newCount = this->GetItemCount(); + this->OnItemsInsertedEx(pfc::bit_array_range(at, count, true), newCount - count, newCount, bSelect); +} \ No newline at end of file
