/***************************************************************************** * * treelist.cpp * * A tree-like listview. (Worst of both worlds!) * *****************************************************************************/ // // state icon: Doesn't get ugly highlight when selected // but indent doesn't work unless there is a small imagelist // // image: gets ugly highlight // but at least indent works #include "sdview.h" TreeItem *TreeItem::NextVisible() { if (IsExpanded()) { return FirstChild(); } TreeItem *pti = this; do { if (pti->NextSibling()) { return pti->NextSibling(); } pti = pti->Parent(); } while (pti); return NULL; } BOOL TreeItem::IsVisibleOrRoot() { TreeItem *pti = Parent(); while (pti) { ASSERT(pti->IsExpandable()); if (!pti->IsExpanded()) { return FALSE; } pti = pti->Parent(); } // Made it all the way to the root without incident return TRUE; } BOOL TreeItem::IsVisible() { TreeItem *pti = Parent(); // // The root itself is not visible. // if (!pti) { return FALSE; } return IsVisibleOrRoot(); } Tree::Tree(TreeItem *ptiRoot) : _ptiRoot(ptiRoot) , _iHint(-1) , _ptiHint(ptiRoot) { if (_ptiRoot) { _ptiRoot->_ptiChild = PTI_ONDEMAND; _ptiRoot->_iVisIndex = -1; _ptiRoot->_iDepth = -1; } } Tree::~Tree() { DeleteNode(_ptiRoot); } void Tree::SetHWND(HWND hwnd) { _hwnd = hwnd; SHFILEINFO sfi; HIMAGELIST himl = ImageList_LoadBitmap(g_hinst, MAKEINTRESOURCE(IDB_PLUS), 16, 0, RGB(0xFF, 0x00, 0xFF)); ListView_SetImageList(_hwnd, himl, LVSIL_STATE); ListView_SetCallbackMask(_hwnd, LVIS_STATEIMAGEMASK | LVIS_OVERLAYMASK); } HIMAGELIST Tree::SetImageList(HIMAGELIST himl) { return RECAST(HIMAGELIST, ListView_SetImageList(_hwnd, himl, LVSIL_SMALL)); } LRESULT Tree::SendNotify(int code, NMHDR *pnm) { pnm->hwndFrom = _hwnd; pnm->code = code; pnm->idFrom = GetDlgCtrlID(_hwnd); return ::SendMessage(GetParent(_hwnd), WM_NOTIFY, pnm->idFrom, RECAST(LPARAM, pnm)); } LRESULT Tree::OnCacheHint(NMLVCACHEHINT *phint) { _ptiHint = IndexToItem(phint->iFrom); _iHint = phint->iFrom; return 0; } // // pti = the first item that needs to be recalced // void Tree::Recalc(TreeItem *pti) { int iItem = pti->_iVisIndex; if (_iHint > iItem) { _iHint = iItem; _ptiHint = pti; } do { pti->_iVisIndex = iItem; pti = pti->NextVisible(); iItem++; } while (pti); } TreeItem* Tree::IndexToItem(int iItem) { int iHave; TreeItem *ptiHave; if (iItem >= _iHint && _ptiHint) { iHave = _iHint; ptiHave = _ptiHint; ASSERT(ptiHave->_iVisIndex == iHave); } else { iHave = -1; ptiHave = _ptiRoot; } while (iHave < iItem && ptiHave) { ASSERT(ptiHave->_iVisIndex == iHave); ptiHave = ptiHave->NextVisible(); iHave++; } return ptiHave; } int Tree::InsertListviewItem(int iItem) { LVITEM lvi; lvi.iItem = iItem; lvi.iSubItem = 0; lvi.mask = 0; return ListView_InsertItem(_hwnd, &lvi); } BOOL Tree::Insert(TreeItem *pti, TreeItem *ptiParent, TreeItem *ptiAfter) { pti->_ptiParent = ptiParent; TreeItem **pptiUpdate; // Convenience: PTI_APPEND appends as last child if (ptiAfter == PTI_APPEND) { ptiAfter = ptiParent->FirstChild(); if (ptiAfter == PTI_ONDEMAND) { ptiAfter = NULL; } else if (ptiAfter) { while (ptiAfter->NextSibling()) { ptiAfter = ptiAfter->NextSibling(); } } } if (ptiAfter) { pti->_iVisIndex = ptiAfter->_iVisIndex + 1; pptiUpdate = &ptiAfter->_ptiNext; } else { pti->_iVisIndex = ptiParent->_iVisIndex + 1; pptiUpdate = &ptiParent->_ptiChild; if (ptiParent->_ptiChild == PTI_ONDEMAND) { ptiParent->_ptiChild = NULL; } } if (ptiParent->IsExpanded()) { if (InsertListviewItem(pti->_iVisIndex) < 0) { return FALSE; } ptiParent->_cVisKids++; } pti->_ptiNext = *pptiUpdate; *pptiUpdate = pti; pti->_iDepth = ptiParent->_iDepth + 1; if (ptiParent->IsExpanded()) { Recalc(pti); } return TRUE; } // // Update the visible kids count for pti and all its parents. // Sop when we find a node that is collapsed (which means // the visible kids counter is no longer being kept track of). // void Tree::UpdateVisibleCounts(TreeItem *pti, int cDelta) { // // Earlying-out the cDelta==0 case is a clear optimization, // and it's actually important in the goofy scenario where // an expand failed (so the item being updated isn't even // expandable any more). // if (cDelta) { do { ASSERT(pti->IsExpandable()); pti->_cVisKids += cDelta; pti = pti->Parent(); } while (pti && pti->IsExpanded()); } } int Tree::Expand(TreeItem *ptiRoot) { if (ptiRoot->IsExpanded()) { return 0; } if (!ptiRoot->IsExpandable()) { return 0; } if (ptiRoot->FirstChild() == PTI_ONDEMAND) { NMTREELIST tl; tl.pti = ptiRoot; SendNotify(TLN_FILLCHILDREN, &tl.hdr); // // If the callback failed to insert any items, then turn the // entry into an unexpandable item. (We need to redraw it // so the new button shows up.) // if (ptiRoot->FirstChild() == PTI_ONDEMAND) { ptiRoot->SetNotExpandable(); } } BOOL fRootVisible = ptiRoot->IsVisibleOrRoot(); TreeItem *pti = ptiRoot->FirstChild(); int iNewIndex = ptiRoot->_iVisIndex + 1; int cExpanded = 0; while (pti) { cExpanded += 1 + pti->_cVisKids; if (fRootVisible) { // Start at -1 so we also include the item itself for (int i = -1; i < pti->_cVisKids; i++) { InsertListviewItem(iNewIndex); iNewIndex++; } } pti = pti->NextSibling(); } UpdateVisibleCounts(ptiRoot, cExpanded); if (fRootVisible) { Recalc(ptiRoot); // Also need to redraw the root item because its button changed ListView_RedrawItems(_hwnd, ptiRoot->_iVisIndex, ptiRoot->_iVisIndex); } return cExpanded; } int Tree::Collapse(TreeItem *ptiRoot) { if (!ptiRoot->IsExpanded()) { return 0; } if (!ptiRoot->IsExpandable()) { return 0; } TreeItem *pti = ptiRoot->FirstChild(); int iDelIndex = ptiRoot->_iVisIndex + 1; int cCollapsed = 0; BOOL fRootVisible = ptiRoot->IsVisibleOrRoot(); // // HACKHACK for some reason, listview in ownerdata mode animates // deletes but not insertions. What's worse, the deletion animation // occurs even if the item being deleted isn't even visible (because // we deleted a screenful of items ahead of it). So let's just disable // redraws while doing collapses. // if (fRootVisible) { SetWindowRedraw(_hwnd, FALSE); } while (pti) { cCollapsed += 1 + pti->_cVisKids; if (fRootVisible) { // Start at -1 so we also include the item itself for (int i = -1; i < pti->_cVisKids; i++) { ListView_DeleteItem(_hwnd, iDelIndex); } } pti = pti->NextSibling(); } UpdateVisibleCounts(ptiRoot, -cCollapsed); if (fRootVisible) { Recalc(ptiRoot); // Also need to redraw the root item because its button changed ListView_RedrawItems(_hwnd, ptiRoot->_iVisIndex, ptiRoot->_iVisIndex); SetWindowRedraw(_hwnd, TRUE); } return cCollapsed; } int Tree::ToggleExpand(TreeItem *pti) { if (pti->IsExpandable()) { if (pti->IsExpanded()) { return -Collapse(pti); } else { return Expand(pti); } } return 0; } void Tree::RedrawItem(TreeItem *pti) { if (pti->IsVisible()) { ListView_RedrawItems(_hwnd, pti->_iVisIndex, pti->_iVisIndex); } } LRESULT Tree::OnClick(NMITEMACTIVATE *pia) { if (pia->iSubItem == 0) { // Maybe it was a click on the +/- button LVHITTESTINFO hti; hti.pt = pia->ptAction; ListView_HitTest(_hwnd, &hti); if (hti.flags & (LVHT_ONITEMICON | LVHT_ONITEMSTATEICON)) { TreeItem *pti = IndexToItem(pia->iItem); if (pti) { ToggleExpand(pti); } } } return 0; } LRESULT Tree::OnItemActivate(int iItem) { NMTREELIST tl; tl.pti = IndexToItem(iItem); if (tl.pti) { SendNotify(TLN_ITEMACTIVATE, &tl.hdr); } return 0; } // // Classic treeview keys: // // Ctrl+(Left, Right, PgUp, Home, PgDn, End, Up, Down) = scroll the // window without changing selection. // // Enter = activate // PgUp, PgDn, Home, End = navigate // Numpad+, Numpad- = expand/collapse // Numpad* = expand all // Left = collapse focus item or move to parent // Right = expand focus item or move down // Backspace = move to parent // // We don't mimic it perfectly, but we get close enough that hopefully // nobody will notice. // LRESULT Tree::OnKeyDown(NMLVKEYDOWN *pkd) { if (GetKeyState(VK_CONTROL) < 0) { // Allow key to go through - listview will do the work } else { TreeItem *pti; switch (pkd->wVKey) { case VK_ADD: pti = GetCurSel(); if (pti) { Expand(pti); } return 1; case VK_SUBTRACT: pti = GetCurSel(); if (pti) { Collapse(pti); } return 1; case VK_LEFT: pti = GetCurSel(); if (pti) { if (pti->IsExpanded()) { Collapse(pti); } else { SetCurSel(pti->Parent()); } } return 1; case VK_BACK: pti = GetCurSel(); if (pti) { SetCurSel(pti->Parent()); } return 1; case VK_RIGHT: pti = GetCurSel(); if (pti) { if (!Expand(pti)) { pti = pti->NextVisible(); if (pti) { SetCurSel(pti); } } } return 1; } } return 0; } // // Convert the item number into a tree item. // LRESULT Tree::OnGetDispInfo(NMLVDISPINFO *plvd) { TreeItem *pti = IndexToItem(plvd->item.iItem); ASSERT(pti); if (!pti) { return 0; } if (plvd->item.mask & LVIF_STATE) { if (pti->IsExpandable()) { // State images are 1-based plvd->item.state |= INDEXTOSTATEIMAGEMASK(pti->IsExpanded() ? 1 : 2); } } if (plvd->item.mask & LVIF_INDENT) { plvd->item.iIndent = pti->_iDepth; } NMTREELIST tl; tl.pti = pti; if (plvd->item.mask & (LVIF_IMAGE | LVIF_STATE)) { tl.iSubItem = -1; tl.cchTextMax = 0; SendNotify(TLN_GETDISPINFO, &tl.hdr); plvd->item.iImage = tl.iSubItem; if (plvd->item.stateMask & LVIS_OVERLAYMASK) { plvd->item.state |= tl.cchTextMax; } } if (plvd->item.mask & LVIF_TEXT) { tl.iSubItem = plvd->item.iSubItem; tl.pszText = plvd->item.pszText; tl.cchTextMax = plvd->item.cchTextMax; SendNotify(TLN_GETDISPINFO, &tl.hdr); plvd->item.pszText = tl.pszText; } return 0; } LRESULT Tree::OnGetInfoTip(NMLVGETINFOTIP *pgit) { TreeItem *pti = IndexToItem(pgit->iItem); ASSERT(pti); if (pti) { NMTREELIST tl; tl.pti = pti; tl.pszText = pgit->pszText; tl.cchTextMax = pgit->cchTextMax; SendNotify(TLN_GETINFOTIP, &tl.hdr); pgit->pszText = tl.pszText; } return 0; } LRESULT Tree::OnGetContextMenu(int iItem) { TreeItem *pti = IndexToItem(iItem); ASSERT(pti); if (pti) { NMTREELIST tl; tl.pti = pti; return SendNotify(TLN_GETCONTEXTMENU, &tl.hdr); } return 0; } LRESULT Tree::OnCopyToClipboard(int iMin, int iMax) { TreeItem *pti = IndexToItem(iMin); ASSERT(pti); if (pti) { TreeItem *ptiMax = IndexToItem(iMax); String str; while (pti != ptiMax) { NMTREELIST tl; tl.pti = pti; tl.pszText = NULL; tl.cchTextMax = 0; SendNotify(TLN_GETINFOTIP, &tl.hdr); if (tl.pszText) { str << tl.pszText << TEXT("\r\n"); } pti = pti->NextVisible(); } SetClipboardText(_hwnd, str); } return 0; } TreeItem *Tree::GetCurSel() { int iItem = ListView_GetCurSel(_hwnd); if (iItem >= 0) { return IndexToItem(iItem); } return NULL; } void Tree::SetCurSel(TreeItem *pti) { if (pti->IsVisible()) { ListView_SetCurSel(_hwnd, pti->_iVisIndex); ListView_EnsureVisible(_hwnd, pti->_iVisIndex, FALSE); } } void Tree::DeleteNode(TreeItem *pti) { if (pti) { // Nuke all the kids, recursively TreeItem *ptiKid = pti->FirstChild(); if (!ptiKid->IsSentinel()) { do { TreeItem *ptiNext = ptiKid->NextSibling(); DeleteNode(ptiKid); ptiKid = ptiNext; } while (ptiKid); } // This is moved to a subroutine so we don't eat stack // in this highly-recursive function. SendDeleteNotify(pti); } } void Tree::SendDeleteNotify(TreeItem *pti) { NMTREELIST tl; tl.pti = pti; SendNotify(TLN_DELETEITEM, &tl.hdr); }