582 lines
16 KiB
C
582 lines
16 KiB
C
#include "ctlspriv.h"
|
|
#include "treeview.h"
|
|
|
|
// in:
|
|
// hItem item to delete
|
|
// flags controls how/what to delete
|
|
// TVDI_NORMAL delete this node and all children
|
|
// TVDI_NONOTIFY don't send notify messages
|
|
// TVDI_CHILDRENONLY just delete the kids (not the item)
|
|
|
|
void NEAR TV_DeleteItemRecurse(PTREE pTree, TREEITEM FAR * hItem, UINT flags)
|
|
{
|
|
TREEITEM FAR *hKid;
|
|
TREEITEM FAR *hNext;
|
|
TREEITEM FAR *hParent;
|
|
|
|
ValidateTreeItem(hItem, FALSE);
|
|
|
|
// remove all kids (and their kids)
|
|
for (hKid = hItem->hKids; hKid; hKid = hNext) {
|
|
hNext = hKid->hNext;
|
|
|
|
// recurse on each child
|
|
TV_DeleteItemRecurse(pTree, hKid, flags & ~TVDI_CHILDRENONLY);
|
|
}
|
|
|
|
if ((flags & TVDI_CHILDRENONLY) || !hItem->hParent)
|
|
return;
|
|
|
|
if (!(flags & TVDI_NONOTIFY)) // BUGBUG: this is not set by anyone
|
|
{
|
|
NM_TREEVIEW nm;
|
|
// Let the app clean up after itself
|
|
nm.itemOld.hItem = hItem;
|
|
nm.itemOld.lParam = hItem->lParam;
|
|
nm.itemNew.mask = 0;
|
|
nm.itemOld.mask = (TVIF_HANDLE | TVIF_PARAM);
|
|
SendNotify(pTree->hwndParent, pTree->hwnd, TVN_DELETEITEM, &nm.hdr);
|
|
}
|
|
|
|
hParent = hItem->hParent;
|
|
Assert(hParent);
|
|
|
|
// unlink ourselves from the parent child chain
|
|
|
|
if (hParent->hKids == hItem) {
|
|
hParent->hKids = hItem->hNext;
|
|
hKid = NULL;
|
|
} else {
|
|
// not the first child, find our previous item (linear search!)
|
|
hKid = TV_GetNextItem(pTree, hItem, TVGN_PREVIOUS);
|
|
Assert(hKid);
|
|
hKid->hNext = hItem->hNext;
|
|
}
|
|
|
|
pTree->cItems--;
|
|
|
|
TV_ScrollBarsAfterRemove(pTree, hItem);
|
|
|
|
if (hItem->lpstr != LPSTR_TEXTCALLBACK)
|
|
Str_Set(&hItem->lpstr, NULL);
|
|
|
|
#ifdef DEBUG
|
|
hItem->dbg_sig = 0;
|
|
#endif
|
|
|
|
// be careful from here down. hItem is unlinked but
|
|
// still has some valid fields
|
|
|
|
// Check to see if the user has deleted one of the
|
|
// special items that is stored in the main tree structure.
|
|
if (hItem == pTree->htiEdit)
|
|
pTree->htiEdit = NULL;
|
|
|
|
if (hItem == pTree->hDropTarget)
|
|
pTree->hDropTarget = NULL;
|
|
|
|
// if the caret escaped the collapsed area and landed on us, push it away
|
|
if (pTree->hCaret == hItem) {
|
|
HTREEITEM hTemp;
|
|
if (hItem->hNext)
|
|
hTemp = hItem->hNext;
|
|
else {
|
|
hTemp = VISIBLE_PARENT(hItem);
|
|
if (!hTemp)
|
|
hTemp = hKid; // set above when we unlinked from the previous item
|
|
}
|
|
// Reset the caret to NULL as to not try to reference our
|
|
// invalidated item.
|
|
pTree->hCaret = NULL;
|
|
TV_SelectItem(pTree, TVGN_CARET, hTemp, !(flags & TVDI_NOSELCHANGE), FALSE, 0);
|
|
Assert(pTree->hCaret != hItem);
|
|
}
|
|
|
|
|
|
// BUGBUG: might want to really do this
|
|
Assert(pTree->hItemPainting != hItem);
|
|
|
|
ControlFree(pTree->hheap, hItem);
|
|
}
|
|
|
|
|
|
|
|
|
|
// Removes the given item and all children from the tree.
|
|
// Special case: if the given item is the hidden root, all children are
|
|
// removed, but the hidden root is NOT removed.
|
|
|
|
// sets cItems
|
|
|
|
|
|
|
|
BOOL NEAR TV_DeleteItem(PTREE pTree, TREEITEM FAR * hItem, UINT flags)
|
|
{
|
|
// BUGBUG: validate hItem
|
|
|
|
if (hItem == TVI_ROOT || !hItem)
|
|
hItem = pTree->hRoot;
|
|
|
|
// BUGUBG: send TVN_DELETEALLITEMS and TVDI_NONOTIFY if they respond
|
|
// if (hItem == pTree->hRoot)
|
|
// etc.
|
|
|
|
ValidateTreeItem(hItem, FALSE);
|
|
|
|
// Collapse first to speed things up (not as much scroll bar recalcs) and
|
|
// to set the top index correctly after the remove.
|
|
TV_Expand(pTree, TVE_COLLAPSE, hItem, FALSE);
|
|
|
|
// Invalidate everything below this item; must be done AFTER setting the
|
|
// selection
|
|
if (pTree->style & TVS_HASLINES) {
|
|
if (hItem->hParent == pTree->hRoot || hItem == pTree->hRoot || ITEM_VISIBLE(hItem->hParent)) {
|
|
if (pTree->fRedraw) {
|
|
InvalidateRect(pTree->hwnd, NULL, TRUE);
|
|
}
|
|
} else {
|
|
TV_ScrollBelow(pTree, hItem->hParent, FALSE, FALSE);
|
|
}
|
|
} else {
|
|
TV_ScrollBelow(pTree, hItem, FALSE, FALSE);
|
|
}
|
|
|
|
// We can pass in the root to clear all items
|
|
if (hItem == pTree->hRoot)
|
|
flags |= TVDI_CHILDRENONLY;
|
|
|
|
TV_DeleteItemRecurse(pTree, hItem, flags);
|
|
|
|
Assert(pTree->hRoot); // didn't go too far, did we?
|
|
|
|
// maybe everything's gone...
|
|
// check out our cleanup job
|
|
if (!pTree->hRoot->hKids) {
|
|
// the tree itself
|
|
Assert(pTree->cItems == 0);
|
|
pTree->cItems = 0; // just removed it all, didn't we?
|
|
|
|
// BUGBUG: this fails because we don't touch hTop if redraw is off
|
|
// in TV_DeleteItemRecurse()
|
|
// AssertMsg(pTree->hTop == NULL, "hTop not NULL, but empty tree");
|
|
pTree->hTop = NULL;
|
|
|
|
AssertMsg(pTree->hCaret == NULL, "hCaret not NULL, but empty tree");
|
|
pTree->hCaret = NULL;
|
|
|
|
pTree->fNameEditPending = FALSE;
|
|
pTree->cxMax = 0;
|
|
pTree->xPos = 0;
|
|
|
|
// the invisible root
|
|
Assert(pTree->hRoot->hNext == NULL);
|
|
pTree->hRoot->hNext = NULL;
|
|
Assert(pTree->hRoot->hParent == NULL);
|
|
pTree->hRoot->hParent = NULL;
|
|
Assert(pTree->hRoot->hKids == NULL);
|
|
pTree->hRoot->hKids = NULL;
|
|
Assert(pTree->hRoot->state & TVIS_EXPANDED);
|
|
pTree->hRoot->state |= TVIS_EXPANDED;
|
|
Assert(pTree->hRoot->iLevel == (BYTE)-1);
|
|
pTree->hRoot->iLevel = (BYTE) -1;
|
|
Assert(pTree->hRoot->iShownIndex == (WORD)-1);
|
|
pTree->hRoot->iShownIndex = (WORD) -1;
|
|
}
|
|
|
|
return TRUE;
|
|
}
|
|
|
|
|
|
|
|
|
|
// Creates the hidden root node for the tree -- all items will trace up to
|
|
// this root, and the first child of the root is the first item in the tree.
|
|
|
|
// sets hRoot
|
|
|
|
|
|
|
|
BOOL NEAR PASCAL TV_CreateRoot(PTREE pTree)
|
|
{
|
|
TREEITEM FAR * hRoot = ControlAlloc(pTree->hheap, sizeof(TREEITEM));
|
|
if (!hRoot)
|
|
return FALSE;
|
|
|
|
// hRoot->hNext = NULL;
|
|
// hRoot->hKids = NULL;
|
|
// hRoot->hParent = NULL;
|
|
hRoot->iLevel = (BYTE) -1;
|
|
hRoot->state = TVIS_EXPANDED;
|
|
hRoot->iShownIndex = (WORD)-1;
|
|
#ifdef DEBUG
|
|
hRoot->dbg_sig = DEBUG_SIG;
|
|
#endif
|
|
|
|
pTree->hRoot = hRoot;
|
|
return TRUE;
|
|
}
|
|
|
|
#ifdef DEBUG
|
|
|
|
void NEAR DumpItem(TREEITEM FAR *hItem)
|
|
{
|
|
LPSTR p;
|
|
|
|
if (hItem->lpstr == LPSTR_TEXTCALLBACK)
|
|
p = "(callback)";
|
|
else if (hItem->lpstr == NULL)
|
|
p = "(null)";
|
|
else
|
|
p = hItem->lpstr;
|
|
|
|
DebugMsg(DM_TRACE, p);
|
|
DebugMsg(DM_TRACE, "\tstate:%4.4x show index:%3d level:%2d kids:%ld lparam:%4.4x",
|
|
hItem->state, hItem->iShownIndex,
|
|
hItem->iLevel, hItem->fKids, hItem->lParam);
|
|
|
|
}
|
|
|
|
#else
|
|
#define DumpItem(hItem)
|
|
#endif
|
|
|
|
|
|
|
|
|
|
// Adds the item described by the given arguments to the tree.
|
|
|
|
// sets hTop, cItems
|
|
|
|
|
|
|
|
TREEITEM FAR * NEAR TV_InsertItem(PTREE pTree, LPTV_INSERTSTRUCT lpis)
|
|
{
|
|
TREEITEM FAR *hNewItem, FAR *hItem;
|
|
TREEITEM FAR *hParent = lpis->hParent;
|
|
TREEITEM FAR *hInsertAfter = lpis->hInsertAfter;
|
|
|
|
if (!lpis)
|
|
return NULL; //BUGBUG: Validate LPTV_INSERTSTRUCT
|
|
|
|
// don't allow undefined bits
|
|
AssertMsg((lpis->item.mask & ~TVIF_ALL) == 0, "Invalid TVIF mask specified");
|
|
|
|
TV_DismissEdit(pTree, FALSE);
|
|
|
|
ValidateTreeItem(hParent, TRUE); // NULL means TVI_ROOT
|
|
ValidateTreeItem(hInsertAfter, FALSE);
|
|
|
|
hNewItem = ControlAlloc(pTree->hheap, sizeof(TREEITEM));
|
|
if (!hNewItem)
|
|
{
|
|
DebugMsg(DM_ERROR, "TreeView: Out of memory");
|
|
return NULL;
|
|
}
|
|
|
|
#ifdef DEBUG
|
|
hNewItem->dbg_sig = DEBUG_SIG;
|
|
#endif
|
|
|
|
|
|
if (lpis->item.mask & TVIF_TEXT)
|
|
{
|
|
|
|
// We will setup the text string next, before we link our self in
|
|
// as to handle the case where we run out of memory and need to
|
|
// destroy ourself without having to unlink.
|
|
|
|
if (!lpis->item.pszText || (lpis->item.pszText == LPSTR_TEXTCALLBACK))
|
|
{
|
|
hNewItem->lpstr = LPSTR_TEXTCALLBACK;
|
|
}
|
|
else
|
|
{
|
|
if (!Str_Set(&hNewItem->lpstr, lpis->item.pszText))
|
|
{
|
|
// Memory allocation failure...
|
|
DebugMsg(DM_ERROR, "TreeView: Out of memory");
|
|
#ifdef DEBUG
|
|
hNewItem->dbg_sig = 0;
|
|
#endif
|
|
ControlFree(pTree->hheap, hNewItem);
|
|
return NULL;
|
|
}
|
|
}
|
|
} else
|
|
Str_Set(&hNewItem->lpstr, c_szNULL);
|
|
|
|
AssertMsg(hNewItem->lpstr != NULL, "Item added with NULL text");
|
|
|
|
if ((hParent == NULL) || (hParent == TVI_ROOT))
|
|
{
|
|
hParent = pTree->hRoot;
|
|
if (!pTree->hTop)
|
|
pTree->hTop = hNewItem;
|
|
}
|
|
else if (!pTree->hRoot->hKids)
|
|
{
|
|
#ifdef DEBUG
|
|
hNewItem->dbg_sig = 0;
|
|
#endif
|
|
ControlFree(pTree->hheap, hNewItem);
|
|
return NULL;
|
|
}
|
|
|
|
// We will do the sort later, so we can handle TEXTCALLBACK things
|
|
if ((hInsertAfter == TVI_FIRST || hInsertAfter == TVI_SORT) || !hParent->hKids)
|
|
{
|
|
hNewItem->hNext = hParent->hKids;
|
|
hParent->hKids = hNewItem;
|
|
}
|
|
else
|
|
{
|
|
// BUGBUG: we should cache the last insert after pointer to try to
|
|
// catch the case of consecutive adds to the end of a node
|
|
|
|
if (hInsertAfter == TVI_LAST)
|
|
for (hItem = hParent->hKids; hItem->hNext; hItem = hItem->hNext)
|
|
;
|
|
else
|
|
{
|
|
for (hItem = hParent->hKids; hItem->hNext; hItem = hItem->hNext)
|
|
if (hItem == hInsertAfter)
|
|
break;
|
|
}
|
|
|
|
hNewItem->hNext = hItem->hNext;
|
|
hItem->hNext = hNewItem;
|
|
}
|
|
|
|
// hNewItem->hKids = NULL;
|
|
hNewItem->hParent = hParent;
|
|
hNewItem->iLevel = hParent->iLevel + 1;
|
|
// hNewItem->iWidth = 0;
|
|
// hNewItem->state = 0;
|
|
if (pTree->hTop == hNewItem)
|
|
hNewItem->iShownIndex = 0; // calc me please!
|
|
else
|
|
hNewItem->iShownIndex = (WORD)-1; // calc me please!
|
|
|
|
if (lpis->item.mask & TVIF_IMAGE)
|
|
hNewItem->iImage = lpis->item.iImage;
|
|
|
|
if (lpis->item.mask & TVIF_SELECTEDIMAGE)
|
|
hNewItem->iSelectedImage = lpis->item.iSelectedImage;
|
|
|
|
if (lpis->item.mask & TVIF_PARAM)
|
|
hNewItem->lParam = lpis->item.lParam;
|
|
|
|
if (lpis->item.mask & TVIF_STATE)
|
|
hNewItem->state = lpis->item.state & lpis->item.stateMask;
|
|
|
|
if ((hNewItem->state & TVIS_BOLD) && !pTree->hFontBold) //$BOLD
|
|
TV_CreateBoldFont(pTree); //$BOLD
|
|
|
|
// DebugMsg(DM_TRACE, "Tree: Inserting i = %d state = %d", TV_StateIndex(&lpis->item), lpis->item.state);
|
|
|
|
if (lpis->item.mask & TVIF_CHILDREN) {
|
|
switch (lpis->item.cChildren) {
|
|
case I_CHILDRENCALLBACK:
|
|
hNewItem->fKids = KIDS_CALLBACK;
|
|
break;
|
|
|
|
case 0:
|
|
hNewItem->fKids = KIDS_FORCE_NO;
|
|
break;
|
|
|
|
default:
|
|
hNewItem->fKids = KIDS_FORCE_YES;
|
|
break;
|
|
}
|
|
}
|
|
|
|
// accept state bits on create?
|
|
// lpis->item.mask & TVIF_STATE
|
|
|
|
pTree->cItems++;
|
|
|
|
// I don't want to do any callbacks until the item is completed
|
|
// so sorting waits until the end
|
|
// special case an only child for speed
|
|
// (hKids && hKids->hNext means more than one child)
|
|
if ((hInsertAfter == TVI_SORT) && hParent->hKids && hParent->hKids->hNext)
|
|
{
|
|
TV_ITEM sThisItem, sNextItem;
|
|
char szThis[64], szNext[64]; // BUGBUG: these are too small
|
|
|
|
sThisItem.pszText = szThis;
|
|
sThisItem.cchTextMax = sizeof(szThis);
|
|
TV_GetItem(pTree, hNewItem, TVIF_TEXT, &sThisItem);
|
|
|
|
sNextItem.pszText = szNext;
|
|
sNextItem.cchTextMax = sizeof(szNext);
|
|
|
|
// We know that the first kid of hParent is hNewItem
|
|
for (hItem = hNewItem->hNext; hItem; hItem = hItem->hNext)
|
|
{
|
|
TV_GetItem(pTree, hItem, TVIF_TEXT, &sNextItem);
|
|
|
|
if (lstrcmpi(sThisItem.pszText, sNextItem.pszText) < 0)
|
|
break;
|
|
|
|
hInsertAfter = hItem;
|
|
}
|
|
|
|
// Check if this is still the first item
|
|
if (hInsertAfter != TVI_SORT)
|
|
{
|
|
// Move this item from the beginning to where it
|
|
// should be
|
|
hParent->hKids = hNewItem->hNext;
|
|
hNewItem->hNext = hInsertAfter->hNext;
|
|
hInsertAfter->hNext = hNewItem;
|
|
}
|
|
}
|
|
|
|
|
|
if ((hNewItem->hNext == pTree->hTop) && !pTree->fVert) {
|
|
|
|
// there's no scrollbars and we got added before the top
|
|
// item. we're now the top.
|
|
hNewItem->iShownIndex = 0;
|
|
pTree->hTop = hNewItem;
|
|
}
|
|
|
|
if (pTree->fRedraw)
|
|
{
|
|
if (TV_ScrollBarsAfterAdd(pTree, hNewItem))
|
|
TV_ScrollBelow(pTree, hNewItem, FALSE, TRUE);
|
|
|
|
// connect the lines, add the buttons, etc. on the item above
|
|
// TV_GetPrevVisItem only works after TV_Scroll* stuff is done
|
|
if (pTree->style & TVS_HASLINES) {
|
|
RECT rc;
|
|
RECT rc2;
|
|
|
|
if (TV_GetItemRect(pTree, hNewItem, &rc, FALSE)) {
|
|
|
|
// find the previous sibling or the parent if no prev sib.
|
|
if (hParent->hKids == hNewItem) {
|
|
hItem = hParent;
|
|
} else {
|
|
hItem = hParent->hKids;
|
|
while ( hItem->hNext != hNewItem ) {
|
|
Assert(hItem->hNext);
|
|
hItem = hItem->hNext;
|
|
}
|
|
}
|
|
|
|
// invalidate from there to the new one
|
|
if (TV_GetItemRect(pTree, hItem, &rc2, FALSE)) {
|
|
rc2.bottom = rc.bottom;
|
|
RedrawWindow(pTree->hwnd, &rc2, NULL, RDW_INVALIDATE | RDW_ERASE);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// DumpItem(hNewItem);
|
|
|
|
return hNewItem;
|
|
}
|
|
|
|
|
|
|
|
|
|
// Frees all allocated memory and objects associated with the tree.
|
|
|
|
|
|
|
|
void NEAR TV_DestroyTree(PTREE pTree)
|
|
{
|
|
HWND hwnd = pTree->hwnd;
|
|
|
|
Assert(pTree->hRoot);
|
|
|
|
pTree->fRedraw = FALSE;
|
|
|
|
// BUGUBG: send TVN_DELETEALLITEMS and TVDI_NONOTIFY if they respond
|
|
TV_DeleteItem(pTree, pTree->hRoot, TVDI_CHILDRENONLY | TVDI_NOSELCHANGE);
|
|
|
|
ControlFree(pTree->hheap, pTree->hRoot);
|
|
|
|
if (pTree->hdcBits)
|
|
{
|
|
if (pTree->hBmp)
|
|
{
|
|
SelectObject(pTree->hdcBits, pTree->hStartBmp);
|
|
DeleteObject(pTree->hBmp);
|
|
}
|
|
|
|
DeleteDC(pTree->hdcBits);
|
|
}
|
|
|
|
if (pTree->fCreatedFont && pTree->hFont)
|
|
DeleteObject(pTree->hFont);
|
|
|
|
if (pTree->hFontBold) //$BOLD
|
|
DeleteObject(pTree->hFontBold); //$BOLD
|
|
|
|
NearFree(pTree);
|
|
|
|
// Don't try to use this var when window is destroyed...
|
|
SetWindowInt(hwnd, 0, 0);
|
|
}
|
|
|
|
|
|
|
|
|
|
// Allocates space for the tree and initializes the tree's data
|
|
|
|
|
|
|
|
LRESULT NEAR TV_OnCreate(HWND hwnd, LPCREATESTRUCT lpCreate)
|
|
{
|
|
PTREE pTree = NearAlloc(sizeof(TREE));
|
|
|
|
if (!pTree)
|
|
return -1; // fail the create window
|
|
|
|
#ifdef WIN32
|
|
pTree->hheap = GetProcessHeap();
|
|
#endif
|
|
|
|
if (!TV_CreateRoot(pTree)) {
|
|
NearFree((HLOCAL)pTree);
|
|
return -1; // fail the create window
|
|
}
|
|
|
|
SetWindowInt(hwnd, 0, (UINT)pTree);
|
|
|
|
pTree->hwnd = hwnd;
|
|
pTree->hwndParent = lpCreate->hwndParent;
|
|
pTree->style = lpCreate->style;
|
|
pTree->fRedraw = TRUE;
|
|
|
|
// pTree->fHorz = FALSE;
|
|
// pTree->fVert = FALSE;
|
|
// pTree->fFocus = FALSE;
|
|
// pTree->fNameEditPending = FALSE;
|
|
// pTree->cxMax = 0;
|
|
// pTree->cxWnd = 0;
|
|
// pTree->cyWnd = 0;
|
|
// pTree->hTop = NULL;
|
|
// pTree->hCaret = NULL;
|
|
// pTree->hDropTarget = NULL;
|
|
// pTree->cItems = 0;
|
|
// pTree->cShowing = 0;
|
|
pTree->cFullVisible = 1;
|
|
// pTree->hdcBits = NULL;
|
|
// pTree->hBmp = NULL;
|
|
// pTree->hbrBk = NULL;
|
|
// pTree->xPos = 0;
|
|
// pTree->cxIndent = 0; // init this for real in TV_OnSetFont()
|
|
|
|
TV_OnSetFont(pTree, NULL, TRUE);
|
|
|
|
SetScrollRange(hwnd, SB_HORZ, 0, 0, TRUE);
|
|
SetScrollRange(hwnd, SB_VERT, 0, 0, TRUE);
|
|
|
|
return 0; // success
|
|
}
|