You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
606 lines
14 KiB
606 lines
14 KiB
/*****************************************************************************
|
|
*
|
|
* 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);
|
|
}
|