2025-05-13 19:45:22 +03:00

6663 lines
188 KiB
C

/*
* Process Hacker -
* tree new (tree list control)
*
* Copyright (C) 2011-2016 wj32
*
* This file is part of Process Hacker.
*
* Process Hacker is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Process Hacker is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Process Hacker. If not, see <http://www.gnu.org/licenses/>.
*/
/*
* The tree new is a tree view with columns. Unlike the old tree list control, which was a wrapper
* around the list view control, this control was written from scratch.
*
* Current issues not included in any comments:
* * Adding, removing or changing columns does not cause invalidation.
* * It is not possible to change a column to make it fixed. The current fixed column must be
* removed and the new fixed column must then be added.
* * When there are no visible normal columns, the space usually occupied by the normal column
* headers is filled with a solid background color. We should catch this and paint the usual
* themed background there instead.
* * It is not possible to update any TN_STYLE_* flags after the control is created.
*
* Possible additions:
* * More flexible mouse input callbacks to allow custom controls inside columns.
* * Allow custom drawn columns to customize their behaviour when TN_FLAG_ITEM_DRAG_SELECT is set
* (e.g. disable drag selection over certain areas).
* * Virtual mode
*/
#include <ph.h>
#include <guisup.h>
#include <windowsx.h>
#include <uxtheme.h>
#include <vssym32.h>
#include <treenew.h>
#include <treenewp.h>
static PVOID ComCtl32Handle;
static LONG SmallIconWidth;
static LONG SmallIconHeight;
BOOLEAN PhTreeNewInitialization(
VOID
)
{
WNDCLASSEX c = { sizeof(c) };
c.style = CS_DBLCLKS | CS_GLOBALCLASS;
c.lpfnWndProc = PhTnpWndProc;
c.cbClsExtra = 0;
c.cbWndExtra = sizeof(PVOID);
c.hInstance = PhLibImageBase;
c.hIcon = NULL;
c.hCursor = LoadCursor(NULL, IDC_ARROW);
c.hbrBackground = NULL;
c.lpszMenuName = NULL;
c.lpszClassName = PH_TREENEW_CLASSNAME;
c.hIconSm = NULL;
if (!RegisterClassEx(&c))
return FALSE;
ComCtl32Handle = GetModuleHandle(L"comctl32.dll");
SmallIconWidth = GetSystemMetrics(SM_CXSMICON);
SmallIconHeight = GetSystemMetrics(SM_CYSMICON);
return TRUE;
}
LRESULT CALLBACK PhTnpWndProc(
_In_ HWND hwnd,
_In_ UINT uMsg,
_In_ WPARAM wParam,
_In_ LPARAM lParam
)
{
PPH_TREENEW_CONTEXT context;
context = (PPH_TREENEW_CONTEXT)GetWindowLongPtr(hwnd, 0);
if (uMsg == WM_CREATE)
{
PhTnpCreateTreeNewContext(&context);
SetWindowLongPtr(hwnd, 0, (LONG_PTR)context);
}
if (!context)
return DefWindowProc(hwnd, uMsg, wParam, lParam);
if (context->Tracking && (GetAsyncKeyState(VK_ESCAPE) & 0x1))
{
PhTnpCancelTrack(context);
}
// Note: if we have suspended restructuring, we *cannot* access any nodes, because all node
// pointers are now invalid. Below, we disable all input.
switch (uMsg)
{
case WM_CREATE:
{
if (!PhTnpOnCreate(hwnd, context, (CREATESTRUCT *)lParam))
return -1;
}
return 0;
case WM_NCDESTROY:
{
context->Callback(hwnd, TreeNewDestroying, NULL, NULL, context->CallbackContext);
PhTnpDestroyTreeNewContext(context);
SetWindowLongPtr(hwnd, 0, (LONG_PTR)NULL);
}
return 0;
case WM_SIZE:
{
PhTnpOnSize(hwnd, context);
}
break;
case WM_ERASEBKGND:
return TRUE;
case WM_PAINT:
{
PhTnpOnPaint(hwnd, context);
}
return 0;
case WM_PRINTCLIENT:
{
if (!context->SuspendUpdateStructure)
PhTnpOnPrintClient(hwnd, context, (HDC)wParam, (ULONG)lParam);
}
return 0;
case WM_NCPAINT:
{
if (PhTnpOnNcPaint(hwnd, context, (HRGN)wParam))
return 0;
}
break;
case WM_GETFONT:
return (LRESULT)context->Font;
case WM_SETFONT:
{
PhTnpOnSetFont(hwnd, context, (HFONT)wParam, LOWORD(lParam));
}
break;
case WM_STYLECHANGED:
{
PhTnpOnStyleChanged(hwnd, context, (LONG)wParam, (STYLESTRUCT *)lParam);
}
break;
case WM_SETTINGCHANGE:
{
PhTnpOnSettingChange(hwnd, context);
}
break;
case WM_THEMECHANGED:
{
PhTnpOnThemeChanged(hwnd, context);
}
break;
case WM_GETDLGCODE:
return PhTnpOnGetDlgCode(hwnd, context, (ULONG)wParam, (PMSG)lParam);
case WM_SETFOCUS:
{
context->HasFocus = TRUE;
InvalidateRect(context->Handle, NULL, FALSE);
}
return 0;
case WM_KILLFOCUS:
{
context->HasFocus = FALSE;
InvalidateRect(context->Handle, NULL, FALSE);
}
return 0;
case WM_SETCURSOR:
{
if (PhTnpOnSetCursor(hwnd, context, (HWND)wParam))
return TRUE;
}
break;
case WM_TIMER:
{
PhTnpOnTimer(hwnd, context, (ULONG)wParam);
}
return 0;
case WM_MOUSEMOVE:
{
if (!context->SuspendUpdateStructure)
PhTnpOnMouseMove(hwnd, context, (ULONG)wParam, GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam));
else
context->SuspendUpdateMoveMouse = TRUE;
}
break;
case WM_MOUSELEAVE:
{
if (!context->SuspendUpdateStructure)
PhTnpOnMouseLeave(hwnd, context);
}
break;
case WM_LBUTTONDOWN:
case WM_LBUTTONUP:
case WM_LBUTTONDBLCLK:
case WM_RBUTTONDOWN:
case WM_RBUTTONUP:
case WM_RBUTTONDBLCLK:
case WM_MBUTTONDOWN:
case WM_MBUTTONUP:
case WM_MBUTTONDBLCLK:
{
if (!context->SuspendUpdateStructure)
PhTnpOnXxxButtonXxx(hwnd, context, uMsg, (ULONG)wParam, GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam));
}
break;
case WM_CAPTURECHANGED:
{
PhTnpOnCaptureChanged(hwnd, context);
}
break;
case WM_KEYDOWN:
{
if (!context->SuspendUpdateStructure)
PhTnpOnKeyDown(hwnd, context, (ULONG)wParam, (ULONG)lParam);
}
break;
case WM_CHAR:
{
if (!context->SuspendUpdateStructure)
PhTnpOnChar(hwnd, context, (ULONG)wParam, (ULONG)lParam);
}
return 0;
case WM_MOUSEWHEEL:
{
PhTnpOnMouseWheel(hwnd, context, (SHORT)HIWORD(wParam), LOWORD(wParam), GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam));
}
break;
case WM_MOUSEHWHEEL:
{
PhTnpOnMouseHWheel(hwnd, context, (SHORT)HIWORD(wParam), LOWORD(wParam), GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam));
}
break;
case WM_CONTEXTMENU:
{
if (!context->SuspendUpdateStructure)
PhTnpOnContextMenu(hwnd, context, GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam));
}
return 0;
case WM_VSCROLL:
{
PhTnpOnVScroll(hwnd, context, LOWORD(wParam), HIWORD(wParam));
}
return 0;
case WM_HSCROLL:
{
PhTnpOnHScroll(hwnd, context, LOWORD(wParam), HIWORD(wParam));
}
return 0;
case WM_NOTIFY:
{
LRESULT result;
if (PhTnpOnNotify(hwnd, context, (NMHDR *)lParam, &result))
return result;
}
break;
}
if (uMsg >= TNM_FIRST && uMsg <= TNM_LAST)
{
return PhTnpOnUserMessage(hwnd, context, uMsg, wParam, lParam);
}
switch (uMsg)
{
case WM_MOUSEMOVE:
case WM_LBUTTONDOWN:
case WM_LBUTTONUP:
case WM_RBUTTONDOWN:
case WM_RBUTTONUP:
case WM_MBUTTONDOWN:
case WM_MBUTTONUP:
{
if (context->TooltipsHandle)
{
MSG message;
message.hwnd = hwnd;
message.message = uMsg;
message.wParam = wParam;
message.lParam = lParam;
SendMessage(context->TooltipsHandle, TTM_RELAYEVENT, 0, (LPARAM)&message);
}
}
break;
}
return DefWindowProc(hwnd, uMsg, wParam, lParam);
}
BOOLEAN NTAPI PhTnpNullCallback(
_In_ HWND hwnd,
_In_ PH_TREENEW_MESSAGE Message,
_In_opt_ PVOID Parameter1,
_In_opt_ PVOID Parameter2,
_In_opt_ PVOID Context
)
{
return FALSE;
}
VOID PhTnpCreateTreeNewContext(
_Out_ PPH_TREENEW_CONTEXT *Context
)
{
PPH_TREENEW_CONTEXT context;
context = PhAllocate(sizeof(PH_TREENEW_CONTEXT));
memset(context, 0, sizeof(PH_TREENEW_CONTEXT));
context->FixedWidthMinimum = 20;
context->RowHeight = 1; // must never be 0
context->HotNodeIndex = -1;
context->Callback = PhTnpNullCallback;
context->FlatList = PhCreateList(64);
context->TooltipIndex = -1;
context->TooltipId = -1;
context->TooltipColumnId = -1;
context->EnableRedraw = 1;
*Context = context;
}
VOID PhTnpDestroyTreeNewContext(
_In_ PPH_TREENEW_CONTEXT Context
)
{
ULONG i;
if (Context->Columns)
{
for (i = 0; i < Context->NextId; i++)
{
if (Context->Columns[i])
PhFree(Context->Columns[i]);
}
PhFree(Context->Columns);
}
if (Context->ColumnsByDisplay)
PhFree(Context->ColumnsByDisplay);
PhDereferenceObject(Context->FlatList);
if (Context->FontOwned)
DeleteObject(Context->Font);
if (Context->ThemeData)
CloseThemeData(Context->ThemeData);
if (Context->SearchString)
PhFree(Context->SearchString);
if (Context->TooltipText)
PhDereferenceObject(Context->TooltipText);
if (Context->BufferedContext)
PhTnpDestroyBufferedContext(Context);
if (Context->SuspendUpdateRegion)
DeleteObject(Context->SuspendUpdateRegion);
PhFree(Context);
}
BOOLEAN PhTnpOnCreate(
_In_ HWND hwnd,
_In_ PPH_TREENEW_CONTEXT Context,
_In_ CREATESTRUCT *CreateStruct
)
{
ULONG headerStyle;
Context->Handle = hwnd;
Context->InstanceHandle = CreateStruct->hInstance;
Context->Style = CreateStruct->style;
Context->ExtendedStyle = CreateStruct->dwExStyle;
if (Context->Style & TN_STYLE_DOUBLE_BUFFERED)
Context->DoubleBuffered = TRUE;
if ((Context->Style & TN_STYLE_ANIMATE_DIVIDER) && Context->DoubleBuffered)
Context->AnimateDivider = TRUE;
headerStyle = HDS_HORZ | HDS_FULLDRAG;
if (!(Context->Style & TN_STYLE_NO_COLUMN_SORT))
headerStyle |= HDS_BUTTONS;
if (!(Context->Style & TN_STYLE_NO_COLUMN_HEADER))
headerStyle |= WS_VISIBLE;
if (!(Context->FixedHeaderHandle = CreateWindow(
WC_HEADER,
NULL,
WS_CHILD | WS_CLIPSIBLINGS | headerStyle,
0,
0,
0,
0,
hwnd,
NULL,
CreateStruct->hInstance,
NULL
)))
{
return FALSE;
}
if (!(Context->Style & TN_STYLE_NO_COLUMN_REORDER))
headerStyle |= HDS_DRAGDROP;
if (!(Context->HeaderHandle = CreateWindow(
WC_HEADER,
NULL,
WS_CHILD | WS_CLIPSIBLINGS | headerStyle,
0,
0,
0,
0,
hwnd,
NULL,
CreateStruct->hInstance,
NULL
)))
{
return FALSE;
}
if (!(Context->VScrollHandle = CreateWindow(
L"SCROLLBAR",
NULL,
WS_CHILD | WS_CLIPSIBLINGS | WS_VISIBLE | SBS_VERT,
0,
0,
0,
0,
hwnd,
NULL,
CreateStruct->hInstance,
NULL
)))
{
return FALSE;
}
if (!(Context->HScrollHandle = CreateWindow(
L"SCROLLBAR",
NULL,
WS_CHILD | WS_CLIPSIBLINGS | WS_VISIBLE | SBS_HORZ,
0,
0,
0,
0,
hwnd,
NULL,
CreateStruct->hInstance,
NULL
)))
{
return FALSE;
}
if (!(Context->FillerBoxHandle = CreateWindow(
L"STATIC",
NULL,
WS_CHILD | WS_CLIPSIBLINGS | WS_VISIBLE,
0,
0,
0,
0,
hwnd,
NULL,
CreateStruct->hInstance,
NULL
)))
{
return FALSE;
}
PhTnpSetFont(Context, NULL, FALSE); // use default font
PhTnpUpdateSystemMetrics(Context);
PhTnpInitializeTooltips(Context);
return TRUE;
}
VOID PhTnpOnSize(
_In_ HWND hwnd,
_In_ PPH_TREENEW_CONTEXT Context
)
{
GetClientRect(hwnd, &Context->ClientRect);
if (Context->BufferedContext && (
Context->BufferedContextRect.right < Context->ClientRect.right ||
Context->BufferedContextRect.bottom < Context->ClientRect.bottom))
{
// Invalidate the buffered context because the client size has increased.
PhTnpDestroyBufferedContext(Context);
}
PhTnpLayout(Context);
if (Context->TooltipsHandle)
{
TOOLINFO toolInfo;
memset(&toolInfo, 0, sizeof(TOOLINFO));
toolInfo.cbSize = sizeof(TOOLINFO);
toolInfo.hwnd = hwnd;
toolInfo.uId = TNP_TOOLTIPS_ITEM;
toolInfo.rect = Context->ClientRect;
SendMessage(Context->TooltipsHandle, TTM_NEWTOOLRECT, 0, (LPARAM)&toolInfo);
}
}
VOID PhTnpOnSetFont(
_In_ HWND hwnd,
_In_ PPH_TREENEW_CONTEXT Context,
_In_opt_ HFONT Font,
_In_ LOGICAL Redraw
)
{
PhTnpSetFont(Context, Font, !!Redraw);
PhTnpLayout(Context);
}
VOID PhTnpOnStyleChanged(
_In_ HWND hwnd,
_In_ PPH_TREENEW_CONTEXT Context,
_In_ LONG Type,
_In_ STYLESTRUCT *StyleStruct
)
{
if (Type == GWL_EXSTYLE)
Context->ExtendedStyle = StyleStruct->styleNew;
}
VOID PhTnpOnSettingChange(
_In_ HWND hwnd,
_In_ PPH_TREENEW_CONTEXT Context
)
{
PhTnpUpdateSystemMetrics(Context);
PhTnpUpdateTextMetrics(Context);
PhTnpLayout(Context);
}
VOID PhTnpOnThemeChanged(
_In_ HWND hwnd,
_In_ PPH_TREENEW_CONTEXT Context
)
{
PhTnpUpdateThemeData(Context);
}
ULONG PhTnpOnGetDlgCode(
_In_ HWND hwnd,
_In_ PPH_TREENEW_CONTEXT Context,
_In_ ULONG VirtualKey,
_In_opt_ PMSG Message
)
{
ULONG code;
if (Context->Callback(hwnd, TreeNewGetDialogCode, UlongToPtr(VirtualKey), &code, Context->CallbackContext))
{
return code;
}
return DLGC_WANTARROWS | DLGC_WANTCHARS;
}
VOID PhTnpOnPaint(
_In_ HWND hwnd,
_In_ PPH_TREENEW_CONTEXT Context
)
{
RECT updateRect;
HDC hdc;
PAINTSTRUCT paintStruct;
if (GetUpdateRect(hwnd, &updateRect, FALSE) && (updateRect.left | updateRect.right | updateRect.top | updateRect.bottom))
{
if (Context->EnableRedraw <= 0)
{
HRGN updateRegion;
updateRegion = CreateRectRgn(0, 0, 0, 0);
GetUpdateRgn(hwnd, updateRegion, FALSE);
if (!Context->SuspendUpdateRegion)
{
Context->SuspendUpdateRegion = updateRegion;
}
else
{
CombineRgn(Context->SuspendUpdateRegion, Context->SuspendUpdateRegion, updateRegion, RGN_OR);
DeleteObject(updateRegion);
}
// Pretend we painted something; this ensures the update region is validated properly.
if (BeginPaint(hwnd, &paintStruct))
EndPaint(hwnd, &paintStruct);
return;
}
if (Context->DoubleBuffered)
{
if (!Context->BufferedContext)
{
PhTnpCreateBufferedContext(Context);
}
}
if (hdc = BeginPaint(hwnd, &paintStruct))
{
updateRect = paintStruct.rcPaint;
if (Context->BufferedContext)
{
PhTnpPaint(hwnd, Context, Context->BufferedContext, &updateRect);
BitBlt(
hdc,
updateRect.left,
updateRect.top,
updateRect.right - updateRect.left,
updateRect.bottom - updateRect.top,
Context->BufferedContext,
updateRect.left,
updateRect.top,
SRCCOPY
);
}
else
{
PhTnpPaint(hwnd, Context, hdc, &updateRect);
}
EndPaint(hwnd, &paintStruct);
}
}
}
VOID PhTnpOnPrintClient(
_In_ HWND hwnd,
_In_ PPH_TREENEW_CONTEXT Context,
_In_ HDC hdc,
_In_ ULONG Flags
)
{
PhTnpPaint(hwnd, Context, hdc, &Context->ClientRect);
}
BOOLEAN PhTnpOnNcPaint(
_In_ HWND hwnd,
_In_ PPH_TREENEW_CONTEXT Context,
_In_opt_ HRGN UpdateRegion
)
{
PhTnpInitializeThemeData(Context);
// Themed border
if ((Context->ExtendedStyle & WS_EX_CLIENTEDGE) && Context->ThemeData)
{
HDC hdc;
ULONG flags;
if (UpdateRegion == HRGN_FULL)
UpdateRegion = NULL;
// Note the use of undocumented flags below. GetDCEx doesn't work without these.
flags = DCX_WINDOW | DCX_LOCKWINDOWUPDATE | 0x10000;
if (UpdateRegion)
flags |= DCX_INTERSECTRGN | 0x40000;
if (hdc = GetDCEx(hwnd, UpdateRegion, flags))
{
PhTnpDrawThemedBorder(Context, hdc);
ReleaseDC(hwnd, hdc);
return TRUE;
}
}
return FALSE;
}
BOOLEAN PhTnpOnSetCursor(
_In_ HWND hwnd,
_In_ PPH_TREENEW_CONTEXT Context,
_In_ HWND CursorWindowHandle
)
{
POINT point;
PhTnpGetMessagePos(hwnd, &point);
if (TNP_HIT_TEST_FIXED_DIVIDER(point.x, Context))
{
if (!Context->DividerCursor)
Context->DividerCursor = LoadCursor(ComCtl32Handle, MAKEINTRESOURCE(106)); // HACK (the divider icon resource has been 106 for quite a while...)
SetCursor(Context->DividerCursor);
return TRUE;
}
if (Context->Cursor)
{
SetCursor(Context->Cursor);
return TRUE;
}
return FALSE;
}
VOID PhTnpOnTimer(
_In_ HWND hwnd,
_In_ PPH_TREENEW_CONTEXT Context,
_In_ ULONG Id
)
{
if (Id == TNP_TIMER_ANIMATE_DIVIDER)
{
RECT dividerRect;
dividerRect.left = Context->FixedWidth;
dividerRect.top = Context->HeaderHeight;
dividerRect.right = Context->FixedWidth + 1;
dividerRect.bottom = Context->ClientRect.bottom;
if (Context->AnimateDividerFadingIn)
{
Context->DividerHot += TNP_ANIMATE_DIVIDER_INCREMENT;
if (Context->DividerHot >= 100)
{
Context->DividerHot = 100;
Context->AnimateDividerFadingIn = FALSE;
KillTimer(hwnd, TNP_TIMER_ANIMATE_DIVIDER);
}
InvalidateRect(hwnd, &dividerRect, FALSE);
}
else if (Context->AnimateDividerFadingOut)
{
if (Context->DividerHot <= TNP_ANIMATE_DIVIDER_DECREMENT)
{
Context->DividerHot = 0;
Context->AnimateDividerFadingOut = FALSE;
KillTimer(hwnd, TNP_TIMER_ANIMATE_DIVIDER);
}
else
{
Context->DividerHot -= TNP_ANIMATE_DIVIDER_DECREMENT;
}
InvalidateRect(hwnd, &dividerRect, FALSE);
}
}
}
VOID PhTnpOnMouseMove(
_In_ HWND hwnd,
_In_ PPH_TREENEW_CONTEXT Context,
_In_ ULONG VirtualKeys,
_In_ LONG CursorX,
_In_ LONG CursorY
)
{
TRACKMOUSEEVENT trackMouseEvent;
trackMouseEvent.cbSize = sizeof(TRACKMOUSEEVENT);
trackMouseEvent.dwFlags = TME_LEAVE;
trackMouseEvent.hwndTrack = hwnd;
trackMouseEvent.dwHoverTime = 0;
TrackMouseEvent(&trackMouseEvent);
if (Context->Tracking)
{
ULONG newFixedWidth;
newFixedWidth = Context->TrackOldFixedWidth + (CursorX - Context->TrackStartX);
PhTnpSetFixedWidth(Context, newFixedWidth);
}
PhTnpProcessMoveMouse(Context, CursorX, CursorY);
}
VOID PhTnpOnMouseLeave(
_In_ HWND hwnd,
_In_ PPH_TREENEW_CONTEXT Context
)
{
RECT rect;
if (Context->HotNodeIndex != -1 && Context->ThemeData)
{
// Update the old hot node because it may have a different non-hot background and plus minus part.
if (PhTnpGetRowRects(Context, Context->HotNodeIndex, Context->HotNodeIndex, TRUE, &rect))
{
InvalidateRect(Context->Handle, &rect, FALSE);
}
}
Context->HotNodeIndex = -1;
if (Context->AnimateDivider && Context->FixedDividerVisible)
{
if ((Context->DividerHot != 0 || Context->AnimateDividerFadingIn) && !Context->AnimateDividerFadingOut)
{
// Fade out the divider.
Context->AnimateDividerFadingOut = TRUE;
Context->AnimateDividerFadingIn = FALSE;
SetTimer(Context->Handle, TNP_TIMER_ANIMATE_DIVIDER, TNP_ANIMATE_DIVIDER_INTERVAL, NULL);
}
}
}
VOID PhTnpOnXxxButtonXxx(
_In_ HWND hwnd,
_In_ PPH_TREENEW_CONTEXT Context,
_In_ ULONG Message,
_In_ ULONG VirtualKeys,
_In_ LONG CursorX,
_In_ LONG CursorY
)
{
BOOLEAN startingTracking;
PH_TREENEW_HIT_TEST hitTest;
LOGICAL controlKey;
LOGICAL shiftKey;
RECT rect;
ULONG changedStart;
ULONG changedEnd;
PH_TREENEW_MESSAGE clickMessage;
// Focus
if (Message == WM_LBUTTONDOWN || Message == WM_RBUTTONDOWN)
SetFocus(hwnd);
// Divider tracking
startingTracking = FALSE;
switch (Message)
{
case WM_LBUTTONDOWN:
{
if (TNP_HIT_TEST_FIXED_DIVIDER(CursorX, Context))
{
startingTracking = TRUE;
Context->Tracking = TRUE;
Context->TrackStartX = CursorX;
Context->TrackOldFixedWidth = Context->FixedWidth;
SetCapture(hwnd);
SetTimer(hwnd, TNP_TIMER_NULL, 100, NULL); // make sure we get messages once in a while so we can detect the escape key
GetAsyncKeyState(VK_ESCAPE);
}
}
break;
case WM_LBUTTONUP:
{
if (Context->Tracking)
{
ReleaseCapture();
}
}
break;
case WM_RBUTTONDOWN:
{
if (Context->Tracking)
{
PhTnpCancelTrack(Context);
}
}
break;
}
if (!startingTracking && Context->Tracking) // still OK to process further if the user is only starting to drag the divider
return;
hitTest.Point.x = CursorX;
hitTest.Point.y = CursorY;
hitTest.InFlags = TN_TEST_COLUMN | TN_TEST_SUBITEM;
PhTnpHitTest(Context, &hitTest);
controlKey = VirtualKeys & MK_CONTROL;
shiftKey = VirtualKeys & MK_SHIFT;
// Plus minus glyph
if ((hitTest.Flags & TN_HIT_ITEM_PLUSMINUS) && Message == WM_LBUTTONDOWN)
{
PhTnpSetExpandedNode(Context, hitTest.Node, !hitTest.Node->Expanded);
}
// Selection
if (!(hitTest.Flags & TN_HIT_ITEM_PLUSMINUS) && (Message == WM_LBUTTONDOWN || Message == WM_RBUTTONDOWN))
{
LOGICAL allowDragSelect;
PH_TREENEW_CELL_PARTS parts;
PhTnpPopTooltip(Context);
allowDragSelect = TRUE;
if (hitTest.Flags & TN_HIT_ITEM)
{
allowDragSelect = FALSE;
Context->FocusNode = hitTest.Node;
if (Context->ExtendedFlags & TN_FLAG_ITEM_DRAG_SELECT)
{
// To allow drag selection to begin even if the cursor is on an item, we check if
// the cursor is on the item icon or text. Exceptions are:
// * When the item is already selected
// * When user is beginning to drag the divider
if (!hitTest.Node->Selected && !startingTracking)
{
if (PhTnpGetCellParts(Context, hitTest.Node->Index, hitTest.Column, TN_MEASURE_TEXT, &parts))
{
allowDragSelect = TRUE;
if ((parts.Flags & TN_PART_ICON) && CursorX >= parts.IconRect.left && CursorX < parts.IconRect.right)
allowDragSelect = FALSE;
if ((parts.Flags & TN_PART_CONTENT) && (parts.Flags & TN_PART_TEXT))
{
if (CursorX >= parts.TextRect.left && CursorX < parts.TextRect.right)
allowDragSelect = FALSE;
}
}
}
}
PhTnpProcessSelectNode(Context, hitTest.Node, controlKey, shiftKey, Message == WM_RBUTTONDOWN);
}
if (allowDragSelect)
{
BOOLEAN dragSelect;
ULONG indexToSelect;
BOOLEAN selectionProcessed;
BOOLEAN showContextMenu;
dragSelect = FALSE;
indexToSelect = -1;
selectionProcessed = FALSE;
showContextMenu = FALSE;
if (!(hitTest.Flags & (TN_HIT_LEFT | TN_HIT_RIGHT | TN_HIT_ABOVE | TN_HIT_BELOW)) && !startingTracking) // don't interfere with divider
{
BOOLEAN result;
ULONG saveIndex;
ULONG saveId;
ULONG cancelledByMessage;
// Check for drag selection. PhTnpDetectDrag has its own message loop, so we need to
// clear our pointers before we continue or we will have some access violations when
// items get deleted.
if (hitTest.Node)
saveIndex = hitTest.Node->Index;
else
saveIndex = -1;
if (hitTest.Column)
saveId = hitTest.Column->Id;
else
saveId = -1;
result = PhTnpDetectDrag(Context, CursorX, CursorY, TRUE, &cancelledByMessage);
// Restore the pointers.
if (saveIndex == -1)
hitTest.Node = NULL;
else if (saveIndex < Context->FlatList->Count)
hitTest.Node = Context->FlatList->Items[saveIndex];
else
return;
if (saveId != -1 && !(hitTest.Column = PhTnpLookupColumnById(Context, saveId)))
return;
if (result)
{
dragSelect = TRUE;
if ((hitTest.Flags & TN_HIT_ITEM) && (Context->ExtendedFlags & TN_FLAG_ITEM_DRAG_SELECT))
{
// Include the current node before starting the drag selection, otherwise
// the user will never be able to select the current node.
indexToSelect = hitTest.Node->Index;
}
}
else
{
if ((Message == WM_LBUTTONDOWN && cancelledByMessage == WM_LBUTTONUP) ||
(Message == WM_RBUTTONDOWN && cancelledByMessage == WM_RBUTTONUP))
{
POINT point;
if ((hitTest.Flags & TN_HIT_ITEM) && (Context->ExtendedFlags & TN_FLAG_ITEM_DRAG_SELECT))
{
// The user isn't performing a drag selection, so prevent deselection.
selectionProcessed = TRUE;
}
// The button up message gets consumed by PhTnpDetectDrag, so send the mouse
// event here.
// Check if the cursor stayed in the same place.
PhTnpGetMessagePos(Context->Handle, &point);
if (point.x == CursorX && point.y == CursorY)
{
PhTnpSendMouseEvent(
Context,
Message == WM_LBUTTONDOWN ? TreeNewLeftClick : TreeNewRightClick,
CursorX,
CursorY,
hitTest.Node,
hitTest.Column,
VirtualKeys
);
}
if (Message == WM_RBUTTONDOWN)
showContextMenu = TRUE;
}
}
}
if (!selectionProcessed && !controlKey && !shiftKey)
{
// Nothing: deselect everything.
PhTnpSelectRange(Context, indexToSelect, indexToSelect, TN_SELECT_RESET, &changedStart, &changedEnd);
if (PhTnpGetRowRects(Context, changedStart, changedEnd, TRUE, &rect))
{
InvalidateRect(hwnd, &rect, FALSE);
}
}
if (dragSelect)
{
PhTnpDragSelect(Context, CursorX, CursorY);
}
if (showContextMenu)
{
SendMessage(Context->Handle, WM_CONTEXTMENU, (WPARAM)Context->Handle, GetMessagePos());
}
return;
}
}
// Click, double-click
// Note: If TN_FLAG_ITEM_DRAG_SELECT is enabled, the code below that processes WM_xBUTTONDOWN
// and WM_xBUTTONUP messages only takes effect when the user clicks directly on an item's icon
// or text.
clickMessage = -1;
if (Message == WM_LBUTTONDOWN || Message == WM_RBUTTONDOWN)
{
if (Context->MouseDownLast != 0 && Context->MouseDownLast != Message)
{
// User pressed one button and pressed the other without letting go of the first one.
// This counts as a click.
if (Context->MouseDownLast == WM_LBUTTONDOWN)
clickMessage = TreeNewLeftClick;
else
clickMessage = TreeNewRightClick;
}
Context->MouseDownLast = Message;
Context->MouseDownLocation.x = CursorX;
Context->MouseDownLocation.y = CursorY;
}
else if (Message == WM_LBUTTONUP || Message == WM_RBUTTONUP)
{
if (Context->MouseDownLast != 0 &&
Context->MouseDownLocation.x == CursorX && Context->MouseDownLocation.y == CursorY)
{
if (Context->MouseDownLast == WM_LBUTTONDOWN)
clickMessage = TreeNewLeftClick;
else
clickMessage = TreeNewRightClick;
}
Context->MouseDownLast = 0;
}
else if (Message == WM_LBUTTONDBLCLK)
{
clickMessage = TreeNewLeftDoubleClick;
}
else if (Message == WM_RBUTTONDBLCLK)
{
clickMessage = TreeNewRightDoubleClick;
}
if (!(hitTest.Flags & TN_HIT_ITEM_PLUSMINUS) && clickMessage != -1)
{
PhTnpSendMouseEvent(Context, clickMessage, CursorX, CursorY, hitTest.Node, hitTest.Column, VirtualKeys);
}
}
VOID PhTnpOnCaptureChanged(
_In_ HWND hwnd,
_In_ PPH_TREENEW_CONTEXT Context
)
{
Context->Tracking = FALSE;
KillTimer(hwnd, TNP_TIMER_NULL);
}
VOID PhTnpOnKeyDown(
_In_ HWND hwnd,
_In_ PPH_TREENEW_CONTEXT Context,
_In_ ULONG VirtualKey,
_In_ ULONG Data
)
{
PH_TREENEW_KEY_EVENT keyEvent;
keyEvent.Handled = FALSE;
keyEvent.VirtualKey = VirtualKey;
keyEvent.Data = Data;
Context->Callback(Context->Handle, TreeNewKeyDown, &keyEvent, NULL, Context->CallbackContext);
if (keyEvent.Handled)
return;
if (PhTnpProcessFocusKey(Context, VirtualKey))
return;
if (PhTnpProcessNodeKey(Context, VirtualKey))
return;
}
VOID PhTnpOnChar(
_In_ HWND hwnd,
_In_ PPH_TREENEW_CONTEXT Context,
_In_ ULONG Character,
_In_ ULONG Data
)
{
// Make sure the character is printable.
if (Character >= ' ' && Character <= '~')
{
PhTnpProcessSearchKey(Context, Character);
}
}
VOID PhTnpOnMouseWheel(
_In_ HWND hwnd,
_In_ PPH_TREENEW_CONTEXT Context,
_In_ LONG Distance,
_In_ ULONG VirtualKeys,
_In_ LONG CursorX,
_In_ LONG CursorY
)
{
// The normal mouse wheel can affect both the vertical scrollbar and the horizontal scrollbar,
// but the vertical scrollbar takes precedence.
if (Context->VScrollVisible)
{
PhTnpProcessMouseVWheel(Context, -Distance);
}
else if (Context->HScrollVisible)
{
PhTnpProcessMouseHWheel(Context, -Distance);
}
}
VOID PhTnpOnMouseHWheel(
_In_ HWND hwnd,
_In_ PPH_TREENEW_CONTEXT Context,
_In_ LONG Distance,
_In_ ULONG VirtualKeys,
_In_ LONG CursorX,
_In_ LONG CursorY
)
{
PhTnpProcessMouseHWheel(Context, Distance);
}
VOID PhTnpOnContextMenu(
_In_ HWND hwnd,
_In_ PPH_TREENEW_CONTEXT Context,
_In_ LONG CursorScreenX,
_In_ LONG CursorScreenY
)
{
POINT clientPoint;
BOOLEAN keyboardInvoked;
PH_TREENEW_HIT_TEST hitTest;
PH_TREENEW_CONTEXT_MENU contextMenu;
if (CursorScreenX == -1 && CursorScreenY == -1)
{
ULONG i;
BOOLEAN found;
RECT windowRect;
RECT rect;
keyboardInvoked = TRUE;
// Context menu was invoked via keyboard. Display the context menu at the selected item.
found = FALSE;
for (i = 0; i < Context->FlatList->Count; i++)
{
if (((PPH_TREENEW_NODE)Context->FlatList->Items[i])->Selected)
{
found = TRUE;
break;
}
}
if (found && PhTnpGetRowRects(Context, i, i, FALSE, &rect) &&
rect.top >= Context->ClientRect.top && rect.top < Context->ClientRect.bottom)
{
clientPoint.x = rect.left + SmallIconWidth / 2;
clientPoint.y = rect.top + Context->RowHeight / 2;
}
else
{
clientPoint.x = 0;
clientPoint.y = 0;
}
GetWindowRect(hwnd, &windowRect);
CursorScreenX = windowRect.left + clientPoint.x;
CursorScreenY = windowRect.top + clientPoint.y;
}
else
{
keyboardInvoked = FALSE;
clientPoint.x = CursorScreenX;
clientPoint.y = CursorScreenY;
ScreenToClient(hwnd, &clientPoint);
if (clientPoint.y < Context->HeaderHeight)
{
// Already handled by TreeNewHeaderRightClick.
return;
}
}
hitTest.Point = clientPoint;
hitTest.InFlags = TN_TEST_COLUMN;
PhTnpHitTest(Context, &hitTest);
contextMenu.Location.x = CursorScreenX;
contextMenu.Location.y = CursorScreenY;
contextMenu.ClientLocation = clientPoint;
contextMenu.Node = hitTest.Node;
contextMenu.Column = hitTest.Column;
contextMenu.KeyboardInvoked = keyboardInvoked;
Context->Callback(hwnd, TreeNewContextMenu, &contextMenu, NULL, Context->CallbackContext);
}
VOID PhTnpOnVScroll(
_In_ HWND hwnd,
_In_ PPH_TREENEW_CONTEXT Context,
_In_ ULONG Request,
_In_ USHORT Position
)
{
SCROLLINFO scrollInfo;
LONG oldPosition;
scrollInfo.cbSize = sizeof(SCROLLINFO);
scrollInfo.fMask = SIF_ALL;
GetScrollInfo(Context->VScrollHandle, SB_CTL, &scrollInfo);
oldPosition = scrollInfo.nPos;
switch (Request)
{
case SB_LINEUP:
scrollInfo.nPos--;
break;
case SB_LINEDOWN:
scrollInfo.nPos++;
break;
case SB_PAGEUP:
scrollInfo.nPos -= scrollInfo.nPage;
break;
case SB_PAGEDOWN:
scrollInfo.nPos += scrollInfo.nPage;
break;
case SB_THUMBPOSITION:
// Touch scrolling seems to give us Position but not nTrackPos. The problem is that Position
// is a 16-bit value, so don't use it if we have too many rows.
if (Context->FlatList->Count <= 0xffff)
scrollInfo.nPos = Position;
break;
case SB_THUMBTRACK:
scrollInfo.nPos = scrollInfo.nTrackPos;
break;
case SB_TOP:
scrollInfo.nPos = 0;
break;
case SB_BOTTOM:
scrollInfo.nPos = MAXINT;
break;
}
scrollInfo.fMask = SIF_POS;
SetScrollInfo(Context->VScrollHandle, SB_CTL, &scrollInfo, TRUE);
GetScrollInfo(Context->VScrollHandle, SB_CTL, &scrollInfo);
if (scrollInfo.nPos != oldPosition)
{
Context->VScrollPosition = scrollInfo.nPos;
PhTnpProcessScroll(Context, scrollInfo.nPos - oldPosition, 0);
}
}
VOID PhTnpOnHScroll(
_In_ HWND hwnd,
_In_ PPH_TREENEW_CONTEXT Context,
_In_ ULONG Request,
_In_ USHORT Position
)
{
SCROLLINFO scrollInfo;
LONG oldPosition;
scrollInfo.cbSize = sizeof(SCROLLINFO);
scrollInfo.fMask = SIF_ALL;
GetScrollInfo(Context->HScrollHandle, SB_CTL, &scrollInfo);
oldPosition = scrollInfo.nPos;
switch (Request)
{
case SB_LINELEFT:
scrollInfo.nPos -= Context->TextMetrics.tmAveCharWidth;
break;
case SB_LINERIGHT:
scrollInfo.nPos += Context->TextMetrics.tmAveCharWidth;
break;
case SB_PAGELEFT:
scrollInfo.nPos -= scrollInfo.nPage;
break;
case SB_PAGERIGHT:
scrollInfo.nPos += scrollInfo.nPage;
break;
case SB_THUMBPOSITION:
// Touch scrolling seems to give us Position but not nTrackPos. The problem is that Position
// is a 16-bit value, so don't use it if we have too many rows.
if (Context->FlatList->Count <= 0xffff)
scrollInfo.nPos = Position;
break;
case SB_THUMBTRACK:
scrollInfo.nPos = scrollInfo.nTrackPos;
break;
case SB_LEFT:
scrollInfo.nPos = 0;
break;
case SB_RIGHT:
scrollInfo.nPos = MAXINT;
break;
}
scrollInfo.fMask = SIF_POS;
SetScrollInfo(Context->HScrollHandle, SB_CTL, &scrollInfo, TRUE);
GetScrollInfo(Context->HScrollHandle, SB_CTL, &scrollInfo);
if (scrollInfo.nPos != oldPosition)
{
Context->HScrollPosition = scrollInfo.nPos;
PhTnpProcessScroll(Context, 0, scrollInfo.nPos - oldPosition);
}
}
BOOLEAN PhTnpOnNotify(
_In_ HWND hwnd,
_In_ PPH_TREENEW_CONTEXT Context,
_In_ NMHDR *Header,
_Out_ LRESULT *Result
)
{
switch (Header->code)
{
case HDN_ITEMCHANGING:
case HDN_ITEMCHANGED:
{
NMHEADER *nmHeader = (NMHEADER *)Header;
if (Header->code == HDN_ITEMCHANGING && Header->hwndFrom == Context->FixedHeaderHandle)
{
if (nmHeader->pitem->mask & HDI_WIDTH)
{
if (Context->FixedColumnVisible)
{
Context->FixedWidth = nmHeader->pitem->cxy - 1;
if (Context->FixedWidth < Context->FixedWidthMinimum)
Context->FixedWidth = Context->FixedWidthMinimum;
Context->NormalLeft = Context->FixedWidth + 1;
nmHeader->pitem->cxy = Context->FixedWidth + 1;
}
else
{
Context->FixedWidth = 0;
Context->NormalLeft = 0;
}
}
}
if (Header->hwndFrom == Context->FixedHeaderHandle || Header->hwndFrom == Context->HeaderHandle)
{
if (nmHeader->pitem->mask & HDI_WIDTH)
{
// A column has been resized. Update our stored information.
PhTnpUpdateColumnHeaders(Context);
PhTnpUpdateColumnMaps(Context);
if (Header->code == HDN_ITEMCHANGING)
{
HDITEM item;
item.mask = HDI_WIDTH | HDI_LPARAM;
if (Header_GetItem(Header->hwndFrom, nmHeader->iItem, &item))
{
Context->ResizingColumn = (PPH_TREENEW_COLUMN)item.lParam;
Context->OldColumnWidth = item.cxy;
}
else
{
Context->ResizingColumn = NULL;
Context->OldColumnWidth = -1;
}
}
else if (Header->code == HDN_ITEMCHANGED)
{
if (Context->ResizingColumn)
{
LONG delta;
delta = nmHeader->pitem->cxy - Context->OldColumnWidth;
if (delta != 0)
{
PhTnpProcessResizeColumn(Context, Context->ResizingColumn, delta);
Context->Callback(Context->Handle, TreeNewColumnResized, Context->ResizingColumn, NULL, Context->CallbackContext);
}
Context->ResizingColumn = NULL;
// Redraw the entire window if we are displaying empty text.
if (Context->FlatList->Count == 0 && Context->EmptyText.Length != 0)
InvalidateRect(Context->Handle, NULL, FALSE);
}
else
{
// An error occurred during HDN_ITEMCHANGED, so redraw the entire window.
InvalidateRect(Context->Handle, NULL, FALSE);
}
}
}
}
}
break;
case HDN_ITEMCLICK:
{
if ((Header->hwndFrom == Context->FixedHeaderHandle || Header->hwndFrom == Context->HeaderHandle) &&
!(Context->Style & TN_STYLE_NO_COLUMN_SORT))
{
NMHEADER *nmHeader = (NMHEADER *)Header;
HDITEM item;
PPH_TREENEW_COLUMN column;
// A column has been clicked, so update the sort state.
item.mask = HDI_LPARAM;
if (Header_GetItem(Header->hwndFrom, nmHeader->iItem, &item))
{
column = (PPH_TREENEW_COLUMN)item.lParam;
PhTnpProcessSortColumn(Context, column);
}
}
}
break;
case HDN_ENDDRAG:
case NM_RELEASEDCAPTURE:
{
if (Header->hwndFrom == Context->HeaderHandle)
{
// Columns have been re-ordered, so refresh our information.
// Note: The fixed column cannot be re-ordered.
PhTnpUpdateColumnHeaders(Context);
PhTnpUpdateColumnMaps(Context);
Context->Callback(Context->Handle, TreeNewColumnReordered, NULL, NULL, Context->CallbackContext);
InvalidateRect(Context->Handle, NULL, FALSE);
}
}
break;
case HDN_DIVIDERDBLCLICK:
{
if (Header->hwndFrom == Context->FixedHeaderHandle || Header->hwndFrom == Context->HeaderHandle)
{
NMHEADER *nmHeader = (NMHEADER *)Header;
HDITEM item;
if (Context->SuspendUpdateStructure)
break;
item.mask = HDI_LPARAM;
if (Header_GetItem(Header->hwndFrom, nmHeader->iItem, &item))
{
PhTnpAutoSizeColumnHeader(
Context,
Header->hwndFrom,
(PPH_TREENEW_COLUMN)item.lParam,
0
);
}
}
}
break;
case NM_RCLICK:
{
if (Header->hwndFrom == Context->FixedHeaderHandle || Header->hwndFrom == Context->HeaderHandle)
{
PH_TREENEW_HEADER_MOUSE_EVENT mouseEvent;
ULONG position;
position = GetMessagePos();
mouseEvent.ScreenLocation.x = GET_X_LPARAM(position);
mouseEvent.ScreenLocation.y = GET_Y_LPARAM(position);
mouseEvent.Location = mouseEvent.ScreenLocation;
ScreenToClient(hwnd, &mouseEvent.Location);
mouseEvent.HeaderLocation = mouseEvent.ScreenLocation;
ScreenToClient(Header->hwndFrom, &mouseEvent.HeaderLocation);
mouseEvent.Column = PhTnpHitTestHeader(Context, Header->hwndFrom == Context->FixedHeaderHandle, &mouseEvent.HeaderLocation, NULL);
Context->Callback(hwnd, TreeNewHeaderRightClick, &mouseEvent, NULL, Context->CallbackContext);
}
}
break;
case TTN_GETDISPINFO:
{
if (Header->hwndFrom == Context->TooltipsHandle)
{
NMTTDISPINFO *info = (NMTTDISPINFO *)Header;
POINT point;
PhTnpGetMessagePos(hwnd, &point);
PhTnpGetTooltipText(Context, &point, &info->lpszText);
}
}
break;
case TTN_SHOW:
{
if (Header->hwndFrom == Context->TooltipsHandle)
{
*Result = PhTnpPrepareTooltipShow(Context);
return TRUE;
}
}
break;
case TTN_POP:
{
if (Header->hwndFrom == Context->TooltipsHandle)
{
PhTnpPrepareTooltipPop(Context);
}
}
break;
}
return FALSE;
}
ULONG_PTR PhTnpOnUserMessage(
_In_ HWND hwnd,
_In_ PPH_TREENEW_CONTEXT Context,
_In_ ULONG Message,
_In_ ULONG_PTR WParam,
_In_ ULONG_PTR LParam
)
{
switch (Message)
{
case TNM_SETCALLBACK:
{
Context->Callback = (PPH_TREENEW_CALLBACK)LParam;
Context->CallbackContext = (PVOID)WParam;
if (!Context->Callback)
Context->Callback = PhTnpNullCallback;
}
return TRUE;
case TNM_NODESSTRUCTURED:
{
if (Context->EnableRedraw <= 0)
{
Context->SuspendUpdateStructure = TRUE;
Context->SuspendUpdateLayout = TRUE;
InvalidateRect(Context->Handle, NULL, FALSE);
return TRUE;
}
PhTnpRestructureNodes(Context);
PhTnpLayout(Context);
InvalidateRect(Context->Handle, NULL, FALSE);
}
return TRUE;
case TNM_ADDCOLUMN:
return PhTnpAddColumn(Context, (PPH_TREENEW_COLUMN)LParam);
case TNM_REMOVECOLUMN:
return PhTnpRemoveColumn(Context, (ULONG)WParam);
case TNM_GETCOLUMN:
return PhTnpCopyColumn(Context, (ULONG)WParam, (PPH_TREENEW_COLUMN)LParam);
case TNM_SETCOLUMN:
{
PPH_TREENEW_COLUMN column = (PPH_TREENEW_COLUMN)LParam;
return PhTnpChangeColumn(Context, (ULONG)WParam, column->Id, column);
}
break;
case TNM_GETCOLUMNORDERARRAY:
{
ULONG count = (ULONG)WParam;
PULONG order = (PULONG)LParam;
ULONG i;
if (count != Context->NumberOfColumnsByDisplay)
return FALSE;
for (i = 0; i < count; i++)
{
order[i] = Context->ColumnsByDisplay[i]->Id;
}
}
return TRUE;
case TNM_SETCOLUMNORDERARRAY:
{
ULONG count = (ULONG)WParam;
PULONG order = (PULONG)LParam;
ULONG i;
PULONG newOrder;
PPH_TREENEW_COLUMN column;
newOrder = PhAllocate(count * sizeof(ULONG));
for (i = 0; i < count; i++)
{
if (!(column = PhTnpLookupColumnById(Context, order[i])))
{
PhFree(newOrder);
return FALSE;
}
newOrder[i] = column->s.ViewIndex;
}
if (!Header_SetOrderArray(Context->HeaderHandle, count, newOrder))
{
PhFree(newOrder);
return FALSE;
}
PhFree(newOrder);
PhTnpUpdateColumnHeaders(Context);
PhTnpUpdateColumnMaps(Context);
}
return TRUE;
case TNM_SETCURSOR:
{
Context->Cursor = (HCURSOR)LParam;
}
return TRUE;
case TNM_GETSORT:
{
PULONG sortColumn = (PULONG)WParam;
PPH_SORT_ORDER sortOrder = (PPH_SORT_ORDER)LParam;
if (sortColumn)
*sortColumn = Context->SortColumn;
if (sortOrder)
*sortOrder = Context->SortOrder;
}
return TRUE;
case TNM_SETSORT:
{
ULONG sortColumn = (ULONG)WParam;
PH_SORT_ORDER sortOrder = (PH_SORT_ORDER)LParam;
PPH_TREENEW_COLUMN column;
if (sortOrder != NoSortOrder)
{
if (!(column = PhTnpLookupColumnById(Context, sortColumn)))
return FALSE;
}
else
{
sortColumn = 0;
column = NULL;
}
Context->SortColumn = sortColumn;
Context->SortOrder = sortOrder;
PhTnpSetColumnHeaderSortIcon(Context, column);
Context->Callback(Context->Handle, TreeNewSortChanged, NULL, NULL, Context->CallbackContext);
}
return TRUE;
case TNM_SETTRISTATE:
Context->TriState = !!WParam;
return TRUE;
case TNM_ENSUREVISIBLE:
return PhTnpEnsureVisibleNode(Context, ((PPH_TREENEW_NODE)LParam)->Index);
case TNM_SCROLL:
PhTnpScroll(Context, (LONG)WParam, (LONG)LParam);
return TRUE;
case TNM_GETFLATNODECOUNT:
if (!Context->SuspendUpdateStructure)
return (LRESULT)Context->FlatList->Count;
else
return 0;
case TNM_GETFLATNODE:
{
ULONG index = (ULONG)WParam;
if (index >= Context->FlatList->Count)
return (LRESULT)NULL;
return (LRESULT)Context->FlatList->Items[index];
}
break;
case TNM_GETCELLTEXT:
{
PPH_TREENEW_GET_CELL_TEXT getCellText = (PPH_TREENEW_GET_CELL_TEXT)LParam;
return PhTnpGetCellText(
Context,
getCellText->Node,
getCellText->Id,
&getCellText->Text
);
}
break;
case TNM_SETNODEEXPANDED:
PhTnpSetExpandedNode(Context, (PPH_TREENEW_NODE)LParam, !!WParam);
return TRUE;
case TNM_GETMAXID:
return (LRESULT)(Context->NextId - 1);
case TNM_SETMAXID:
{
ULONG maxId = (ULONG)WParam;
if (Context->NextId < maxId + 1)
{
Context->NextId = maxId + 1;
if (Context->AllocatedColumns < Context->NextId)
{
PhTnpExpandAllocatedColumns(Context);
}
}
}
return TRUE;
case TNM_INVALIDATENODE:
{
PPH_TREENEW_NODE node = (PPH_TREENEW_NODE)LParam;
RECT rect;
if (!node->Visible)
return FALSE;
if (!PhTnpGetRowRects(Context, node->Index, node->Index, TRUE, &rect))
return FALSE;
InvalidateRect(hwnd, &rect, FALSE);
}
return TRUE;
case TNM_INVALIDATENODES:
{
RECT rect;
if (!PhTnpGetRowRects(Context, (ULONG)WParam, (ULONG)LParam, TRUE, &rect))
return FALSE;
InvalidateRect(hwnd, &rect, FALSE);
}
return TRUE;
case TNM_GETFIXEDHEADER:
return (LRESULT)Context->FixedHeaderHandle;
case TNM_GETHEADER:
return (LRESULT)Context->HeaderHandle;
case TNM_GETTOOLTIPS:
return (LRESULT)Context->TooltipsHandle;
case TNM_SELECTRANGE:
case TNM_DESELECTRANGE:
{
ULONG flags;
ULONG changedStart;
ULONG changedEnd;
RECT rect;
flags = 0;
if (Message == TNM_DESELECTRANGE)
flags |= TN_SELECT_DESELECT;
PhTnpSelectRange(Context, (ULONG)WParam, (ULONG)LParam, flags, &changedStart, &changedEnd);
if (PhTnpGetRowRects(Context, changedStart, changedEnd, TRUE, &rect))
{
InvalidateRect(hwnd, &rect, FALSE);
}
}
return TRUE;
case TNM_GETCOLUMNCOUNT:
return (LRESULT)Context->NumberOfColumns;
case TNM_SETREDRAW:
PhTnpSetRedraw(Context, !!WParam);
return (LRESULT)Context->EnableRedraw;
case TNM_GETVIEWPARTS:
{
PPH_TREENEW_VIEW_PARTS parts = (PPH_TREENEW_VIEW_PARTS)LParam;
parts->ClientRect = Context->ClientRect;
parts->HeaderHeight = Context->HeaderHeight;
parts->RowHeight = Context->RowHeight;
parts->VScrollWidth = Context->VScrollVisible ? Context->VScrollWidth : 0;
parts->HScrollHeight = Context->HScrollHeight ? Context->HScrollHeight : 0;
parts->VScrollPosition = Context->VScrollPosition;
parts->HScrollPosition = Context->HScrollPosition;
parts->FixedWidth = Context->FixedWidth;
parts->NormalLeft = Context->NormalLeft;
parts->NormalWidth = Context->TotalViewX;
}
return TRUE;
case TNM_GETFIXEDCOLUMN:
return (LRESULT)Context->FixedColumn;
case TNM_GETFIRSTCOLUMN:
return (LRESULT)Context->FirstColumn;
case TNM_SETFOCUSNODE:
Context->FocusNode = (PPH_TREENEW_NODE)LParam;
return TRUE;
case TNM_SETMARKNODE:
Context->MarkNodeIndex = ((PPH_TREENEW_NODE)LParam)->Index;
return TRUE;
case TNM_SETHOTNODE:
PhTnpSetHotNode(Context, (PPH_TREENEW_NODE)LParam, FALSE);
return TRUE;
case TNM_SETEXTENDEDFLAGS:
Context->ExtendedFlags = (Context->ExtendedFlags & ~(ULONG)WParam) | ((ULONG)LParam & (ULONG)WParam);
return TRUE;
case TNM_GETCALLBACK:
{
PPH_TREENEW_CALLBACK *callback = (PPH_TREENEW_CALLBACK *)LParam;
PVOID *callbackContext = (PVOID *)WParam;
if (callback)
{
if (Context->Callback != PhTnpNullCallback)
*callback = Context->Callback;
else
*callback = NULL;
}
if (callbackContext)
{
*callbackContext = Context->CallbackContext;
}
}
return TRUE;
case TNM_HITTEST:
PhTnpHitTest(Context, (PPH_TREENEW_HIT_TEST)LParam);
return TRUE;
case TNM_GETVISIBLECOLUMNCOUNT:
return Context->NumberOfColumnsByDisplay + (Context->FixedColumnVisible ? 1 : 0);
case TNM_AUTOSIZECOLUMN:
{
ULONG id = (ULONG)WParam;
ULONG flags = (ULONG)LParam;
PPH_TREENEW_COLUMN column;
if (!(column = PhTnpLookupColumnById(Context, id)))
return FALSE;
if (!column->Visible)
return FALSE;
PhTnpAutoSizeColumnHeader(
Context,
column->Fixed ? Context->FixedHeaderHandle : Context->HeaderHandle,
column,
flags
);
}
return TRUE;
case TNM_SETEMPTYTEXT:
{
PPH_STRINGREF text = (PPH_STRINGREF)LParam;
ULONG flags = (ULONG)WParam;
Context->EmptyText = *text;
}
return TRUE;
case TNM_SETROWHEIGHT:
{
LONG rowHeight = (LONG)WParam;
if (rowHeight != 0)
{
Context->CustomRowHeight = TRUE;
Context->RowHeight = rowHeight;
}
else
{
Context->CustomRowHeight = FALSE;
PhTnpUpdateTextMetrics(Context);
}
}
return TRUE;
case TNM_ISFLATNODEVALID:
return !Context->SuspendUpdateStructure;
}
return 0;
}
VOID PhTnpSetFont(
_In_ PPH_TREENEW_CONTEXT Context,
_In_opt_ HFONT Font,
_In_ BOOLEAN Redraw
)
{
if (Context->FontOwned)
{
DeleteObject(Context->Font);
Context->FontOwned = FALSE;
}
Context->Font = Font;
if (!Context->Font)
{
LOGFONT logFont;
if (SystemParametersInfo(SPI_GETICONTITLELOGFONT, sizeof(LOGFONT), &logFont, 0))
{
Context->Font = CreateFontIndirect(&logFont);
Context->FontOwned = TRUE;
}
}
SendMessage(Context->FixedHeaderHandle, WM_SETFONT, (WPARAM)Context->Font, Redraw);
SendMessage(Context->HeaderHandle, WM_SETFONT, (WPARAM)Context->Font, Redraw);
if (Context->TooltipsHandle)
{
SendMessage(Context->TooltipsHandle, WM_SETFONT, (WPARAM)Context->Font, FALSE);
Context->TooltipFont = Context->Font;
}
PhTnpUpdateTextMetrics(Context);
}
VOID PhTnpUpdateSystemMetrics(
_In_ PPH_TREENEW_CONTEXT Context
)
{
Context->VScrollWidth = GetSystemMetrics(SM_CXVSCROLL);
Context->HScrollHeight = GetSystemMetrics(SM_CYHSCROLL);
Context->SystemBorderX = GetSystemMetrics(SM_CXBORDER);
Context->SystemBorderY = GetSystemMetrics(SM_CYBORDER);
Context->SystemEdgeX = GetSystemMetrics(SM_CXEDGE);
Context->SystemEdgeY = GetSystemMetrics(SM_CYEDGE);
Context->SystemDragX = GetSystemMetrics(SM_CXDRAG);
Context->SystemDragY = GetSystemMetrics(SM_CYDRAG);
if (Context->SystemDragX < 2)
Context->SystemDragX = 2;
if (Context->SystemDragY < 2)
Context->SystemDragY = 2;
}
VOID PhTnpUpdateTextMetrics(
_In_ PPH_TREENEW_CONTEXT Context
)
{
HDC hdc;
if (hdc = GetDC(Context->Handle))
{
SelectObject(hdc, Context->Font);
GetTextMetrics(hdc, &Context->TextMetrics);
if (!Context->CustomRowHeight)
{
// Below we try to match the row height as calculated by the list view, even if it
// involves magic numbers. On Vista and above there seems to be extra padding.
Context->RowHeight = Context->TextMetrics.tmHeight;
if (Context->Style & TN_STYLE_ICONS)
{
if (Context->RowHeight < SmallIconHeight)
Context->RowHeight = SmallIconHeight;
}
else
{
if (WindowsVersion >= WINDOWS_VISTA && !(Context->Style & TN_STYLE_THIN_ROWS))
Context->RowHeight += 1; // HACK
}
Context->RowHeight += 1; // HACK
if (WindowsVersion >= WINDOWS_VISTA && !(Context->Style & TN_STYLE_THIN_ROWS))
Context->RowHeight += 2; // HACK
}
ReleaseDC(Context->Handle, hdc);
}
}
VOID PhTnpUpdateThemeData(
_In_ PPH_TREENEW_CONTEXT Context
)
{
Context->ThemeActive = !!IsThemeActive();
if (Context->ThemeData)
{
CloseThemeData(Context->ThemeData);
Context->ThemeData = NULL;
}
Context->ThemeData = OpenThemeData(Context->Handle, L"TREEVIEW");
if (Context->ThemeData)
{
Context->ThemeHasItemBackground = !!IsThemePartDefined(Context->ThemeData, TVP_TREEITEM, 0);
Context->ThemeHasGlyph = !!IsThemePartDefined(Context->ThemeData, TVP_GLYPH, 0);
Context->ThemeHasHotGlyph = !!IsThemePartDefined(Context->ThemeData, TVP_HOTGLYPH, 0);
}
else
{
Context->ThemeHasItemBackground = FALSE;
Context->ThemeHasGlyph = FALSE;
Context->ThemeHasHotGlyph = FALSE;
}
}
VOID PhTnpInitializeThemeData(
_In_ PPH_TREENEW_CONTEXT Context
)
{
if (!Context->ThemeInitialized)
{
PhTnpUpdateThemeData(Context);
Context->ThemeInitialized = TRUE;
}
}
VOID PhTnpCancelTrack(
_In_ PPH_TREENEW_CONTEXT Context
)
{
PhTnpSetFixedWidth(Context, Context->TrackOldFixedWidth);
ReleaseCapture();
}
VOID PhTnpLayout(
_In_ PPH_TREENEW_CONTEXT Context
)
{
RECT clientRect;
if (Context->EnableRedraw <= 0)
{
Context->SuspendUpdateLayout = TRUE;
return;
}
clientRect = Context->ClientRect;
PhTnpUpdateScrollBars(Context);
// Vertical scroll bar
if (Context->VScrollVisible)
{
MoveWindow(
Context->VScrollHandle,
clientRect.right - Context->VScrollWidth,
0,
Context->VScrollWidth,
clientRect.bottom - (Context->HScrollVisible ? Context->HScrollHeight : 0),
TRUE
);
}
// Horizontal scroll bar
if (Context->HScrollVisible)
{
MoveWindow(
Context->HScrollHandle,
Context->NormalLeft,
clientRect.bottom - Context->HScrollHeight,
clientRect.right - Context->NormalLeft - (Context->VScrollVisible ? Context->VScrollWidth : 0),
Context->HScrollHeight,
TRUE
);
}
// Filler box
if (Context->VScrollVisible && Context->HScrollVisible)
{
MoveWindow(
Context->FillerBoxHandle,
clientRect.right - Context->VScrollWidth,
clientRect.bottom - Context->HScrollHeight,
Context->VScrollWidth,
Context->HScrollHeight,
TRUE
);
}
PhTnpLayoutHeader(Context);
// Redraw the entire window if we are displaying empty text.
if (Context->FlatList->Count == 0 && Context->EmptyText.Length != 0)
InvalidateRect(Context->Handle, NULL, FALSE);
}
VOID PhTnpLayoutHeader(
_In_ PPH_TREENEW_CONTEXT Context
)
{
RECT rect;
HDLAYOUT hdl;
WINDOWPOS windowPos;
hdl.prc = &rect;
hdl.pwpos = &windowPos;
if (!(Context->Style & TN_STYLE_NO_COLUMN_HEADER))
{
// Fixed portion header control
rect.left = 0;
rect.top = 0;
rect.right = Context->NormalLeft;
rect.bottom = Context->ClientRect.bottom;
Header_Layout(Context->FixedHeaderHandle, &hdl);
SetWindowPos(Context->FixedHeaderHandle, NULL, windowPos.x, windowPos.y, windowPos.cx, windowPos.cy, windowPos.flags);
Context->HeaderHeight = windowPos.cy;
// Normal portion header control
rect.left = Context->NormalLeft - Context->HScrollPosition;
rect.top = 0;
rect.right = Context->ClientRect.right - (Context->VScrollVisible ? Context->VScrollWidth : 0);
rect.bottom = Context->ClientRect.bottom;
Header_Layout(Context->HeaderHandle, &hdl);
SetWindowPos(Context->HeaderHandle, NULL, windowPos.x, windowPos.y, windowPos.cx, windowPos.cy, windowPos.flags);
}
else
{
Context->HeaderHeight = 0;
}
if (Context->TooltipsHandle)
{
TOOLINFO toolInfo;
memset(&toolInfo, 0, sizeof(TOOLINFO));
toolInfo.cbSize = sizeof(TOOLINFO);
toolInfo.hwnd = Context->FixedHeaderHandle;
toolInfo.uId = TNP_TOOLTIPS_FIXED_HEADER;
GetClientRect(Context->FixedHeaderHandle, &toolInfo.rect);
SendMessage(Context->TooltipsHandle, TTM_NEWTOOLRECT, 0, (LPARAM)&toolInfo);
toolInfo.hwnd = Context->HeaderHandle;
toolInfo.uId = TNP_TOOLTIPS_HEADER;
GetClientRect(Context->HeaderHandle, &toolInfo.rect);
SendMessage(Context->TooltipsHandle, TTM_NEWTOOLRECT, 0, (LPARAM)&toolInfo);
}
}
VOID PhTnpSetFixedWidth(
_In_ PPH_TREENEW_CONTEXT Context,
_In_ ULONG FixedWidth
)
{
HDITEM item;
if (Context->FixedColumnVisible)
{
Context->FixedWidth = FixedWidth;
if (Context->FixedWidth < Context->FixedWidthMinimum)
Context->FixedWidth = Context->FixedWidthMinimum;
Context->NormalLeft = Context->FixedWidth + 1;
item.mask = HDI_WIDTH;
item.cxy = Context->FixedWidth + 1;
Header_SetItem(Context->FixedHeaderHandle, 0, &item);
}
else
{
Context->FixedWidth = 0;
Context->NormalLeft = 0;
}
}
VOID PhTnpSetRedraw(
_In_ PPH_TREENEW_CONTEXT Context,
_In_ BOOLEAN Redraw
)
{
if (Redraw)
Context->EnableRedraw++;
else
Context->EnableRedraw--;
if (Context->EnableRedraw == 1)
{
if (Context->SuspendUpdateStructure)
{
PhTnpRestructureNodes(Context);
}
if (Context->SuspendUpdateLayout)
{
PhTnpLayout(Context);
}
if (Context->SuspendUpdateMoveMouse)
{
POINT point;
PhTnpGetMessagePos(Context->Handle, &point);
PhTnpProcessMoveMouse(Context, point.x, point.y);
}
Context->SuspendUpdateStructure = FALSE;
Context->SuspendUpdateLayout = FALSE;
Context->SuspendUpdateMoveMouse = FALSE;
if (Context->SuspendUpdateRegion)
{
InvalidateRgn(Context->Handle, Context->SuspendUpdateRegion, FALSE);
DeleteObject(Context->SuspendUpdateRegion);
Context->SuspendUpdateRegion = NULL;
}
}
}
VOID PhTnpSendMouseEvent(
_In_ PPH_TREENEW_CONTEXT Context,
_In_ PH_TREENEW_MESSAGE Message,
_In_ LONG CursorX,
_In_ LONG CursorY,
_In_ PPH_TREENEW_NODE Node,
_In_ PPH_TREENEW_COLUMN Column,
_In_ ULONG VirtualKeys
)
{
PH_TREENEW_MOUSE_EVENT mouseEvent;
mouseEvent.Location.x = CursorX;
mouseEvent.Location.y = CursorY;
mouseEvent.Node = Node;
mouseEvent.Column = Column;
mouseEvent.KeyFlags = VirtualKeys;
Context->Callback(Context->Handle, Message, &mouseEvent, NULL, Context->CallbackContext);
}
PPH_TREENEW_COLUMN PhTnpLookupColumnById(
_In_ PPH_TREENEW_CONTEXT Context,
_In_ ULONG Id
)
{
if (Id >= Context->AllocatedColumns)
return NULL;
return Context->Columns[Id];
}
BOOLEAN PhTnpAddColumn(
_In_ PPH_TREENEW_CONTEXT Context,
_In_ PPH_TREENEW_COLUMN Column
)
{
PPH_TREENEW_COLUMN realColumn;
// Check if a column with the same ID already exists.
if (Column->Id < Context->AllocatedColumns && Context->Columns[Column->Id])
return FALSE;
if (Context->NextId < Column->Id + 1)
Context->NextId = Column->Id + 1;
realColumn = PhAllocateCopy(Column, sizeof(PH_TREENEW_COLUMN));
if (realColumn->DpiScaleOnAdd)
{
realColumn->Width = PhMultiplyDivide(realColumn->Width, PhGlobalDpi, 96);
realColumn->DpiScaleOnAdd = FALSE;
}
if (Context->AllocatedColumns < Context->NextId)
{
PhTnpExpandAllocatedColumns(Context);
}
Context->Columns[Column->Id] = realColumn;
Context->NumberOfColumns++;
if (realColumn->Fixed)
{
if (Context->FixedColumn)
{
// We already have a fixed column, and we can't have two. Make this new column un-fixed.
realColumn->Fixed = FALSE;
}
else
{
Context->FixedColumn = realColumn;
}
realColumn->DisplayIndex = 0;
realColumn->s.ViewX = 0;
}
if (realColumn->Visible)
{
BOOLEAN updateHeaders;
updateHeaders = FALSE;
if (!realColumn->Fixed && realColumn->DisplayIndex != Header_GetItemCount(Context->HeaderHandle))
updateHeaders = TRUE;
realColumn->s.ViewIndex = PhTnpInsertColumnHeader(Context, realColumn);
if (updateHeaders)
PhTnpUpdateColumnHeaders(Context);
}
else
{
realColumn->s.ViewIndex = -1;
}
PhTnpUpdateColumnMaps(Context);
if (realColumn->Visible)
PhTnpLayout(Context);
return TRUE;
}
BOOLEAN PhTnpRemoveColumn(
_In_ PPH_TREENEW_CONTEXT Context,
_In_ ULONG Id
)
{
PPH_TREENEW_COLUMN realColumn;
BOOLEAN updateLayout;
if (!(realColumn = PhTnpLookupColumnById(Context, Id)))
return FALSE;
updateLayout = FALSE;
if (realColumn->Visible)
updateLayout = TRUE;
PhTnpDeleteColumnHeader(Context, realColumn);
Context->Columns[realColumn->Id] = NULL;
PhFree(realColumn);
PhTnpUpdateColumnMaps(Context);
if (updateLayout)
PhTnpLayout(Context);
Context->NumberOfColumns--;
return TRUE;
}
BOOLEAN PhTnpCopyColumn(
_In_ PPH_TREENEW_CONTEXT Context,
_In_ ULONG Id,
_Out_ PPH_TREENEW_COLUMN Column
)
{
PPH_TREENEW_COLUMN realColumn;
if (!(realColumn = PhTnpLookupColumnById(Context, Id)))
return FALSE;
memcpy(Column, realColumn, sizeof(PH_TREENEW_COLUMN));
return TRUE;
}
BOOLEAN PhTnpChangeColumn(
_In_ PPH_TREENEW_CONTEXT Context,
_In_ ULONG Mask,
_In_ ULONG Id,
_In_ PPH_TREENEW_COLUMN Column
)
{
PPH_TREENEW_COLUMN realColumn;
BOOLEAN addingOrRemoving;
if (!(realColumn = PhTnpLookupColumnById(Context, Id)))
return FALSE;
addingOrRemoving = FALSE;
if (Mask & TN_COLUMN_FLAG_VISIBLE)
{
if (realColumn->Visible != Column->Visible)
{
addingOrRemoving = TRUE;
}
}
if (Mask & TN_COLUMN_FLAG_CUSTOMDRAW)
{
realColumn->CustomDraw = Column->CustomDraw;
}
if (Mask & TN_COLUMN_FLAG_SORTDESCENDING)
{
realColumn->SortDescending = Column->SortDescending;
}
if (Mask & (TN_COLUMN_TEXT | TN_COLUMN_WIDTH | TN_COLUMN_ALIGNMENT | TN_COLUMN_DISPLAYINDEX))
{
BOOLEAN updateHeaders;
BOOLEAN updateMaps;
BOOLEAN updateLayout;
updateHeaders = FALSE;
updateMaps = FALSE;
updateLayout = FALSE;
if (Mask & TN_COLUMN_TEXT)
{
realColumn->Text = Column->Text;
}
if (Mask & TN_COLUMN_WIDTH)
{
realColumn->Width = Column->Width;
updateMaps = TRUE;
}
if (Mask & TN_COLUMN_ALIGNMENT)
{
realColumn->Alignment = Column->Alignment;
}
if (Mask & TN_COLUMN_DISPLAYINDEX)
{
realColumn->DisplayIndex = Column->DisplayIndex;
updateHeaders = TRUE;
updateMaps = TRUE;
updateLayout = TRUE;
}
if (!addingOrRemoving && realColumn->Visible)
{
PhTnpChangeColumnHeader(Context, Mask, realColumn);
if (updateHeaders)
PhTnpUpdateColumnHeaders(Context);
if (updateMaps)
PhTnpUpdateColumnMaps(Context);
if (updateLayout)
PhTnpLayout(Context);
}
}
if (Mask & TN_COLUMN_CONTEXT)
{
realColumn->Context = Column->Context;
}
if (Mask & TN_COLUMN_TEXTFLAGS)
{
realColumn->TextFlags = Column->TextFlags;
}
if (addingOrRemoving)
{
if (Column->Visible)
{
BOOLEAN updateHeaders;
updateHeaders = FALSE;
if (realColumn->Fixed)
{
realColumn->DisplayIndex = 0;
}
else
{
if (Mask & TN_COLUMN_DISPLAYINDEX)
updateHeaders = TRUE;
else
realColumn->DisplayIndex = Header_GetItemCount(Context->HeaderHandle);
}
realColumn->s.ViewIndex = PhTnpInsertColumnHeader(Context, realColumn);
if (updateHeaders)
PhTnpUpdateColumnHeaders(Context);
}
else
{
PhTnpDeleteColumnHeader(Context, realColumn);
}
PhTnpUpdateColumnMaps(Context);
PhTnpLayout(Context);
}
return TRUE;
}
VOID PhTnpExpandAllocatedColumns(
_In_ PPH_TREENEW_CONTEXT Context
)
{
if (Context->Columns)
{
ULONG oldAllocatedColumns;
oldAllocatedColumns = Context->AllocatedColumns;
Context->AllocatedColumns *= 2;
if (Context->AllocatedColumns < Context->NextId)
Context->AllocatedColumns = Context->NextId;
Context->Columns = PhReAllocate(
Context->Columns,
Context->AllocatedColumns * sizeof(PPH_TREENEW_COLUMN)
);
// Zero the newly allocated portion.
memset(
&Context->Columns[oldAllocatedColumns],
0,
(Context->AllocatedColumns - oldAllocatedColumns) * sizeof(PPH_TREENEW_COLUMN)
);
}
else
{
Context->AllocatedColumns = 16;
if (Context->AllocatedColumns < Context->NextId)
Context->AllocatedColumns = Context->NextId;
Context->Columns = PhAllocate(
Context->AllocatedColumns * sizeof(PPH_TREENEW_COLUMN)
);
memset(Context->Columns, 0, Context->AllocatedColumns * sizeof(PPH_TREENEW_COLUMN));
}
}
VOID PhTnpUpdateColumnMaps(
_In_ PPH_TREENEW_CONTEXT Context
)
{
ULONG i;
LONG x;
if (Context->AllocatedColumnsByDisplay < Context->NumberOfColumns)
{
if (Context->ColumnsByDisplay)
PhFree(Context->ColumnsByDisplay);
Context->ColumnsByDisplay = PhAllocate(sizeof(PPH_TREENEW_COLUMN) * Context->NumberOfColumns);
Context->AllocatedColumnsByDisplay = Context->NumberOfColumns;
}
memset(Context->ColumnsByDisplay, 0, sizeof(PPH_TREENEW_COLUMN) * Context->AllocatedColumnsByDisplay);
for (i = 0; i < Context->NextId; i++)
{
if (!Context->Columns[i])
continue;
if (Context->Columns[i]->Visible && !Context->Columns[i]->Fixed && Context->Columns[i]->DisplayIndex != -1)
{
if (Context->Columns[i]->DisplayIndex >= Context->NumberOfColumns)
PhRaiseStatus(STATUS_INTERNAL_ERROR);
Context->ColumnsByDisplay[Context->Columns[i]->DisplayIndex] = Context->Columns[i];
}
}
x = 0;
for (i = 0; i < Context->AllocatedColumnsByDisplay; i++)
{
if (!Context->ColumnsByDisplay[i])
break;
Context->ColumnsByDisplay[i]->s.ViewX = x;
x += Context->ColumnsByDisplay[i]->Width;
}
Context->NumberOfColumnsByDisplay = i;
Context->TotalViewX = x;
if (Context->FixedColumnVisible)
Context->FirstColumn = Context->FixedColumn;
else if (Context->NumberOfColumnsByDisplay != 0)
Context->FirstColumn = Context->ColumnsByDisplay[0];
else
Context->FirstColumn = NULL;
if (Context->NumberOfColumnsByDisplay != 0)
Context->LastColumn = Context->ColumnsByDisplay[Context->NumberOfColumnsByDisplay - 1];
else if (Context->FixedColumnVisible)
Context->LastColumn = Context->FixedColumn;
else
Context->LastColumn = NULL;
}
LONG PhTnpInsertColumnHeader(
_In_ PPH_TREENEW_CONTEXT Context,
_In_ PPH_TREENEW_COLUMN Column
)
{
HDITEM item;
if (Column->Fixed)
{
if (Column->Width < Context->FixedWidthMinimum)
Column->Width = Context->FixedWidthMinimum;
Context->FixedWidth = Column->Width;
Context->NormalLeft = Context->FixedWidth + 1;
Context->FixedColumnVisible = TRUE;
if (!(Context->Style & TN_STYLE_NO_DIVIDER))
Context->FixedDividerVisible = TRUE;
}
memset(&item, 0, sizeof(HDITEM));
item.mask = HDI_WIDTH | HDI_TEXT | HDI_FORMAT | HDI_LPARAM | HDI_ORDER;
item.cxy = Column->Width;
item.pszText = Column->Text;
item.fmt = 0;
item.lParam = (LPARAM)Column;
if (Column->Fixed)
item.cxy++;
if (Column->Fixed)
item.iOrder = 0;
else
item.iOrder = Column->DisplayIndex;
if (Column->Alignment & PH_ALIGN_LEFT)
item.fmt |= HDF_LEFT;
else if (Column->Alignment & PH_ALIGN_RIGHT)
item.fmt |= HDF_RIGHT;
else
item.fmt |= HDF_CENTER;
if (Column->Id == Context->SortColumn)
{
if (Context->SortOrder == AscendingSortOrder)
item.fmt |= HDF_SORTUP;
else if (Context->SortOrder == DescendingSortOrder)
item.fmt |= HDF_SORTDOWN;
}
Column->Visible = TRUE;
if (Column->Fixed)
return Header_InsertItem(Context->FixedHeaderHandle, 0, &item);
else
return Header_InsertItem(Context->HeaderHandle, MAXINT, &item);
}
VOID PhTnpChangeColumnHeader(
_In_ PPH_TREENEW_CONTEXT Context,
_In_ ULONG Mask,
_In_ PPH_TREENEW_COLUMN Column
)
{
HDITEM item;
memset(&item, 0, sizeof(HDITEM));
item.mask = 0;
if (Mask & TN_COLUMN_TEXT)
{
item.mask |= HDI_TEXT;
item.pszText = Column->Text;
}
if (Mask & TN_COLUMN_WIDTH)
{
item.mask |= HDI_WIDTH;
item.cxy = Column->Width;
if (Column->Fixed)
item.cxy++;
}
if (Mask & TN_COLUMN_ALIGNMENT)
{
item.mask |= HDI_FORMAT;
item.fmt = 0;
if (Column->Alignment & PH_ALIGN_LEFT)
item.fmt |= HDF_LEFT;
else if (Column->Alignment & PH_ALIGN_RIGHT)
item.fmt |= HDF_RIGHT;
else
item.fmt |= HDF_CENTER;
if (Column->Id == Context->SortColumn)
{
if (Context->SortOrder == AscendingSortOrder)
item.fmt |= HDF_SORTUP;
else if (Context->SortOrder == DescendingSortOrder)
item.fmt |= HDF_SORTDOWN;
}
}
if (Mask & TN_COLUMN_DISPLAYINDEX)
{
item.mask |= HDI_ORDER;
if (Column->Fixed)
item.iOrder = 0;
else
item.iOrder = Column->DisplayIndex;
}
if (Column->Fixed)
Header_SetItem(Context->FixedHeaderHandle, 0, &item);
else
Header_SetItem(Context->HeaderHandle, Column->s.ViewIndex, &item);
}
VOID PhTnpDeleteColumnHeader(
_In_ PPH_TREENEW_CONTEXT Context,
_Inout_ PPH_TREENEW_COLUMN Column
)
{
if (Column->Fixed)
{
Context->FixedColumn = NULL;
Context->FixedWidth = 0;
Context->NormalLeft = 0;
Context->FixedColumnVisible = FALSE;
Context->FixedDividerVisible = FALSE;
}
if (Column->Fixed)
Header_DeleteItem(Context->FixedHeaderHandle, Column->s.ViewIndex);
else
Header_DeleteItem(Context->HeaderHandle, Column->s.ViewIndex);
Column->Visible = FALSE;
Column->s.ViewIndex = -1;
PhTnpUpdateColumnHeaders(Context);
}
VOID PhTnpUpdateColumnHeaders(
_In_ PPH_TREENEW_CONTEXT Context
)
{
ULONG count;
ULONG i;
HDITEM item;
PPH_TREENEW_COLUMN column;
item.mask = HDI_WIDTH | HDI_LPARAM | HDI_ORDER;
// Fixed column
if (Context->FixedColumnVisible && Header_GetItem(Context->FixedHeaderHandle, 0, &item))
{
column = Context->FixedColumn;
column->Width = item.cxy - 1;
}
// Normal columns
count = Header_GetItemCount(Context->HeaderHandle);
if (count != -1)
{
for (i = 0; i < count; i++)
{
if (Header_GetItem(Context->HeaderHandle, i, &item))
{
column = (PPH_TREENEW_COLUMN)item.lParam;
column->s.ViewIndex = i;
column->Width = item.cxy;
column->DisplayIndex = item.iOrder;
}
}
}
}
VOID PhTnpProcessResizeColumn(
_In_ PPH_TREENEW_CONTEXT Context,
_In_ PPH_TREENEW_COLUMN Column,
_In_ LONG Delta
)
{
RECT rect;
LONG columnLeft;
if (Column->Fixed)
{
columnLeft = 0;
}
else
{
columnLeft = Context->NormalLeft + Column->s.ViewX - Context->HScrollPosition;
}
// Scroll the content to the right of the column.
//
// Clip the scroll area to the new width, or the old width if that is further to the left. We
// may have the WS_CLIPCHILDREN style set, so we need to remove the horizontal scrollbar from
// the rectangle, otherwise ScrollWindowEx will want to invalidate the entire region! (The
// horizontal scrollbar is an overlapping child control.)
rect.left = columnLeft + Column->Width;
rect.top = Context->HeaderHeight;
rect.right = Context->ClientRect.right - (Context->VScrollVisible ? Context->VScrollWidth : 0);
rect.bottom = Context->ClientRect.bottom - (Context->HScrollVisible ? Context->HScrollHeight : 0);
if (Delta > 0)
rect.left -= Delta; // old width
// Scroll the window.
ScrollWindowEx(
Context->Handle,
Delta,
0,
&rect,
&rect,
NULL,
NULL,
SW_INVALIDATE
);
UpdateWindow(Context->Handle); // required
if (Context->HScrollVisible)
{
// We excluded the bottom region - invalidate it now.
rect.top = rect.bottom;
rect.bottom = Context->ClientRect.bottom;
InvalidateRect(Context->Handle, &rect, FALSE);
}
PhTnpLayout(Context);
// Redraw the whole column because the content may depend on the width (e.g. text ellipsis).
rect.left = columnLeft;
rect.top = Context->HeaderHeight;
rect.right = columnLeft + Column->Width;
RedrawWindow(Context->Handle, &rect, NULL, RDW_INVALIDATE | RDW_UPDATENOW); // must be RedrawWindow
}
VOID PhTnpProcessSortColumn(
_In_ PPH_TREENEW_CONTEXT Context,
_In_ PPH_TREENEW_COLUMN NewColumn
)
{
if (NewColumn->Id == Context->SortColumn)
{
if (Context->TriState)
{
if (!NewColumn->SortDescending)
{
// Ascending -> Descending -> None
if (Context->SortOrder == AscendingSortOrder)
Context->SortOrder = DescendingSortOrder;
else if (Context->SortOrder == DescendingSortOrder)
Context->SortOrder = NoSortOrder;
else
Context->SortOrder = AscendingSortOrder;
}
else
{
// Descending -> Ascending -> None
if (Context->SortOrder == DescendingSortOrder)
Context->SortOrder = AscendingSortOrder;
else if (Context->SortOrder == AscendingSortOrder)
Context->SortOrder = NoSortOrder;
else
Context->SortOrder = DescendingSortOrder;
}
}
else
{
if (Context->SortOrder == AscendingSortOrder)
Context->SortOrder = DescendingSortOrder;
else
Context->SortOrder = AscendingSortOrder;
}
}
else
{
Context->SortColumn = NewColumn->Id;
if (!NewColumn->SortDescending)
Context->SortOrder = AscendingSortOrder;
else
Context->SortOrder = DescendingSortOrder;
}
PhTnpSetColumnHeaderSortIcon(Context, NewColumn);
Context->Callback(Context->Handle, TreeNewSortChanged, NULL, NULL, Context->CallbackContext);
}
BOOLEAN PhTnpSetColumnHeaderSortIcon(
_In_ PPH_TREENEW_CONTEXT Context,
_In_opt_ PPH_TREENEW_COLUMN SortColumnPointer
)
{
if (Context->SortOrder == NoSortOrder)
{
PhSetHeaderSortIcon(
Context->FixedHeaderHandle,
-1,
NoSortOrder
);
PhSetHeaderSortIcon(
Context->HeaderHandle,
-1,
NoSortOrder
);
return TRUE;
}
if (!SortColumnPointer)
{
if (!(SortColumnPointer = PhTnpLookupColumnById(Context, Context->SortColumn)))
return FALSE;
}
if (SortColumnPointer->Fixed)
{
PhSetHeaderSortIcon(
Context->FixedHeaderHandle,
0,
Context->SortOrder
);
PhSetHeaderSortIcon(
Context->HeaderHandle,
-1,
NoSortOrder
);
}
else
{
PhSetHeaderSortIcon(
Context->FixedHeaderHandle,
-1,
NoSortOrder
);
PhSetHeaderSortIcon(
Context->HeaderHandle,
SortColumnPointer->s.ViewIndex,
Context->SortOrder
);
}
return TRUE;
}
VOID PhTnpAutoSizeColumnHeader(
_In_ PPH_TREENEW_CONTEXT Context,
_In_ HWND HeaderHandle,
_In_ PPH_TREENEW_COLUMN Column,
_In_ ULONG Flags
)
{
LONG newWidth;
HDITEM item;
if (Flags & TN_AUTOSIZE_REMAINING_SPACE)
{
newWidth = Context->ClientRect.right - (Context->TotalViewX - Column->Width);
if (Context->FixedColumn)
newWidth -= Context->FixedColumn->Width;
if (Context->VScrollVisible)
newWidth -= Context->VScrollWidth;
if (newWidth <= 0)
return;
}
else
{
ULONG i;
LONG maximumWidth;
PH_TREENEW_CELL_PARTS parts;
LONG width;
if (Context->FlatList->Count == 0)
return;
if (Column->CustomDraw)
return;
maximumWidth = 0;
for (i = 0; i < Context->FlatList->Count; i++)
{
if (PhTnpGetCellParts(Context, i, Column, TN_MEASURE_TEXT, &parts) &&
(parts.Flags & TN_PART_CELL) && (parts.Flags & TN_PART_CONTENT) && (parts.Flags & TN_PART_TEXT))
{
width = parts.TextRect.right - parts.TextRect.left; // text width
width += parts.ContentRect.left - parts.CellRect.left; // left padding
if (maximumWidth < width)
maximumWidth = width;
}
}
newWidth = maximumWidth + TNP_CELL_RIGHT_MARGIN; // right padding
if (Column->Fixed)
newWidth++;
}
item.mask = HDI_WIDTH;
item.cxy = newWidth;
Header_SetItem(HeaderHandle, Column->s.ViewIndex, &item);
}
BOOLEAN PhTnpGetNodeChildren(
_In_ PPH_TREENEW_CONTEXT Context,
_In_opt_ PPH_TREENEW_NODE Node,
_Out_ PPH_TREENEW_NODE **Children,
_Out_ PULONG NumberOfChildren
)
{
PH_TREENEW_GET_CHILDREN getChildren;
getChildren.Flags = 0;
getChildren.Node = Node;
getChildren.Children = NULL;
getChildren.NumberOfChildren = 0;
if (Context->Callback(
Context->Handle,
TreeNewGetChildren,
&getChildren,
NULL,
Context->CallbackContext
))
{
*Children = getChildren.Children;
*NumberOfChildren = getChildren.NumberOfChildren;
return TRUE;
}
return FALSE;
}
BOOLEAN PhTnpIsNodeLeaf(
_In_ PPH_TREENEW_CONTEXT Context,
_In_ PPH_TREENEW_NODE Node
)
{
PH_TREENEW_IS_LEAF isLeaf;
isLeaf.Flags = 0;
isLeaf.Node = Node;
isLeaf.IsLeaf = TRUE;
if (Context->Callback(
Context->Handle,
TreeNewIsLeaf,
&isLeaf,
NULL,
Context->CallbackContext
))
{
return isLeaf.IsLeaf;
}
// Doesn't matter, decide when we do the get-children callback.
return FALSE;
}
BOOLEAN PhTnpGetCellText(
_In_ PPH_TREENEW_CONTEXT Context,
_In_ PPH_TREENEW_NODE Node,
_In_ ULONG Id,
_Out_ PPH_STRINGREF Text
)
{
PH_TREENEW_GET_CELL_TEXT getCellText;
if (Id < Node->TextCacheSize && Node->TextCache[Id].Buffer)
{
*Text = Node->TextCache[Id];
return TRUE;
}
getCellText.Flags = 0;
getCellText.Node = Node;
getCellText.Id = Id;
PhInitializeEmptyStringRef(&getCellText.Text);
if (Context->Callback(
Context->Handle,
TreeNewGetCellText,
&getCellText,
NULL,
Context->CallbackContext
) && getCellText.Text.Buffer)
{
*Text = getCellText.Text;
if ((getCellText.Flags & TN_CACHE) && Id < Node->TextCacheSize)
Node->TextCache[Id] = getCellText.Text;
return TRUE;
}
return FALSE;
}
VOID PhTnpRestructureNodes(
_In_ PPH_TREENEW_CONTEXT Context
)
{
PPH_TREENEW_NODE *children;
ULONG numberOfChildren;
ULONG i;
if (!PhTnpGetNodeChildren(Context, NULL, &children, &numberOfChildren))
return;
// We try to preserve the hot node, the focused node and the selection mark node. At this point
// all node pointers must be regarded as invalid, so we must not follow any pointers.
Context->FocusNodeFound = FALSE;
PhClearList(Context->FlatList);
Context->CanAnyExpand = FALSE;
for (i = 0; i < numberOfChildren; i++)
{
PhTnpInsertNodeChildren(Context, children[i], 0);
}
if (!Context->FocusNodeFound)
Context->FocusNode = NULL; // focused node is no longer present
if (Context->HotNodeIndex >= Context->FlatList->Count) // covers -1 case as well
Context->HotNodeIndex = -1;
if (Context->MarkNodeIndex >= Context->FlatList->Count)
Context->MarkNodeIndex = -1;
}
VOID PhTnpInsertNodeChildren(
_In_ PPH_TREENEW_CONTEXT Context,
_In_ PPH_TREENEW_NODE Node,
_In_ ULONG Level
)
{
PPH_TREENEW_NODE *children;
ULONG numberOfChildren;
ULONG i;
ULONG nextLevel;
if (Node->Visible)
{
Node->Level = Level;
Node->Index = Context->FlatList->Count;
PhAddItemList(Context->FlatList, Node);
if (Context->FocusNode == Node)
Context->FocusNodeFound = TRUE;
nextLevel = Level + 1;
}
else
{
nextLevel = 0; // children of this node should be level 0
}
if (!(Node->s.IsLeaf = PhTnpIsNodeLeaf(Context, Node)))
{
Context->CanAnyExpand = TRUE;
if (Node->Expanded)
{
if (PhTnpGetNodeChildren(Context, Node, &children, &numberOfChildren))
{
for (i = 0; i < numberOfChildren; i++)
{
PhTnpInsertNodeChildren(Context, children[i], nextLevel);
}
if (numberOfChildren == 0)
Node->s.IsLeaf = TRUE;
}
}
}
}
VOID PhTnpSetExpandedNode(
_In_ PPH_TREENEW_CONTEXT Context,
_In_ PPH_TREENEW_NODE Node,
_In_ BOOLEAN Expanded
)
{
if (Node->Expanded != Expanded)
{
PH_TREENEW_NODE_EVENT nodeEvent;
memset(&nodeEvent, 0, sizeof(PH_TREENEW_NODE_EVENT));
Context->Callback(Context->Handle, TreeNewNodeExpanding, Node, &nodeEvent, Context->CallbackContext);
if (!nodeEvent.Handled)
{
if (!Expanded)
{
ULONG i;
PPH_TREENEW_NODE node;
BOOLEAN changed;
// Make sure no children are selected - we don't want invisible selected nodes. Note
// that this does not cause any UI changes by itself, since we are hiding the nodes.
changed = FALSE;
for (i = Node->Index + 1; i < Context->FlatList->Count; i++)
{
node = Context->FlatList->Items[i];
if (node->Level <= Node->Level)
break; // no more children
if (node->Selected)
{
node->Selected = FALSE;
changed = TRUE;
}
}
if (changed)
{
Context->Callback(Context->Handle, TreeNewSelectionChanged, NULL, NULL, Context->CallbackContext);
}
}
Node->Expanded = Expanded;
PhTnpRestructureNodes(Context);
// We need to update the window before the scrollbars get updated in order for the
// scroll processing to work properly.
InvalidateRect(Context->Handle, NULL, FALSE);
UpdateWindow(Context->Handle);
PhTnpLayout(Context);
}
}
}
BOOLEAN PhTnpGetCellParts(
_In_ PPH_TREENEW_CONTEXT Context,
_In_ ULONG Index,
_In_opt_ PPH_TREENEW_COLUMN Column,
_In_ ULONG Flags,
_Out_ PPH_TREENEW_CELL_PARTS Parts
)
{
PPH_TREENEW_NODE node;
LONG viewWidth;
LONG nodeY;
LONG iconVerticalMargin;
LONG currentX;
if (Index >= Context->FlatList->Count)
return FALSE;
node = Context->FlatList->Items[Index];
nodeY = Context->HeaderHeight + ((LONG)Index - Context->VScrollPosition) * Context->RowHeight;
Parts->Flags = 0;
Parts->RowRect.left = 0;
Parts->RowRect.right = Context->NormalLeft + Context->TotalViewX - Context->HScrollPosition;
Parts->RowRect.top = nodeY;
Parts->RowRect.bottom = nodeY + Context->RowHeight;
viewWidth = Context->ClientRect.right - (Context->VScrollVisible ? Context->VScrollWidth : 0);
if (Parts->RowRect.right > viewWidth)
Parts->RowRect.right = viewWidth;
if (!Column)
return TRUE;
if (!Column->Visible)
return FALSE;
iconVerticalMargin = (Context->RowHeight - SmallIconHeight) / 2;
if (Column->Fixed)
{
currentX = 0;
}
else
{
currentX = Context->NormalLeft + Column->s.ViewX - Context->HScrollPosition;
}
Parts->Flags |= TN_PART_CELL;
Parts->CellRect.left = currentX;
Parts->CellRect.right = currentX + Column->Width;
Parts->CellRect.top = Parts->RowRect.top;
Parts->CellRect.bottom = Parts->RowRect.bottom;
currentX += TNP_CELL_LEFT_MARGIN;
if (Column == Context->FirstColumn)
{
currentX += (LONG)node->Level * SmallIconWidth;
if (Context->CanAnyExpand)
{
if (!node->s.IsLeaf)
{
Parts->Flags |= TN_PART_PLUSMINUS;
Parts->PlusMinusRect.left = currentX;
Parts->PlusMinusRect.right = currentX + SmallIconWidth;
Parts->PlusMinusRect.top = Parts->RowRect.top + iconVerticalMargin;
Parts->PlusMinusRect.bottom = Parts->RowRect.bottom - iconVerticalMargin;
}
currentX += SmallIconWidth;
}
if (node->Icon)
{
Parts->Flags |= TN_PART_ICON;
Parts->IconRect.left = currentX;
Parts->IconRect.right = currentX + SmallIconWidth;
Parts->IconRect.top = Parts->RowRect.top + iconVerticalMargin;
Parts->IconRect.bottom = Parts->RowRect.bottom - iconVerticalMargin;
currentX += SmallIconWidth + TNP_ICON_RIGHT_PADDING;
}
}
Parts->Flags |= TN_PART_CONTENT;
Parts->ContentRect.left = currentX;
Parts->ContentRect.right = Parts->CellRect.right - TNP_CELL_RIGHT_MARGIN;
Parts->ContentRect.top = Parts->RowRect.top;
Parts->ContentRect.bottom = Parts->RowRect.bottom;
if (Flags & TN_MEASURE_TEXT)
{
HDC hdc;
PH_STRINGREF text;
HFONT font;
SIZE textSize;
if (hdc = GetDC(Context->Handle))
{
PhTnpPrepareRowForDraw(Context, hdc, node);
if (PhTnpGetCellText(Context, node, Column->Id, &text))
{
if (node->Font)
font = node->Font;
else
font = Context->Font;
SelectObject(hdc, font);
if (GetTextExtentPoint32(hdc, text.Buffer, (ULONG)text.Length / sizeof(WCHAR), &textSize))
{
Parts->Flags |= TN_PART_TEXT;
Parts->TextRect.left = currentX;
Parts->TextRect.right = currentX + textSize.cx;
Parts->TextRect.top = Parts->RowRect.top + (Context->RowHeight - textSize.cy) / 2;
Parts->TextRect.bottom = Parts->RowRect.bottom - (Context->RowHeight - textSize.cy) / 2;
if (Column->TextFlags & DT_CENTER)
{
Parts->TextRect.left = Parts->ContentRect.left / 2 + (Parts->ContentRect.right - textSize.cx) / 2;
Parts->TextRect.right = Parts->ContentRect.left + textSize.cx;
}
else if (Column->TextFlags & DT_RIGHT)
{
Parts->TextRect.right = Parts->ContentRect.right;
Parts->TextRect.left = Parts->TextRect.right - textSize.cx;
}
Parts->Text = text;
Parts->Font = font;
}
}
ReleaseDC(Context->Handle, hdc);
}
}
return TRUE;
}
BOOLEAN PhTnpGetRowRects(
_In_ PPH_TREENEW_CONTEXT Context,
_In_ ULONG Start,
_In_ ULONG End,
_In_ BOOLEAN Clip,
_Out_ PRECT Rect
)
{
LONG startY;
LONG endY;
LONG viewWidth;
if (End >= Context->FlatList->Count)
return FALSE;
if (Start > End)
return FALSE;
startY = Context->HeaderHeight + ((LONG)Start - Context->VScrollPosition) * Context->RowHeight;
endY = Context->HeaderHeight + ((LONG)End - Context->VScrollPosition) * Context->RowHeight;
Rect->left = 0;
Rect->right = Context->NormalLeft + Context->TotalViewX - Context->HScrollPosition;
Rect->top = startY;
Rect->bottom = endY + Context->RowHeight;
viewWidth = Context->ClientRect.right - (Context->VScrollVisible ? Context->VScrollWidth : 0);
if (Rect->right > viewWidth)
Rect->right = viewWidth;
if (Clip)
{
if (Rect->top < Context->HeaderHeight)
Rect->top = Context->HeaderHeight;
if (Rect->bottom > Context->ClientRect.bottom)
Rect->bottom = Context->ClientRect.bottom;
}
return TRUE;
}
VOID PhTnpHitTest(
_In_ PPH_TREENEW_CONTEXT Context,
_Inout_ PPH_TREENEW_HIT_TEST HitTest
)
{
RECT clientRect;
LONG x;
LONG y;
ULONG index;
PPH_TREENEW_NODE node;
HitTest->Flags = 0;
HitTest->Node = NULL;
HitTest->Column = NULL;
clientRect = Context->ClientRect;
x = HitTest->Point.x;
y = HitTest->Point.y;
if (x < 0)
HitTest->Flags |= TN_HIT_LEFT;
if (x >= clientRect.right)
HitTest->Flags |= TN_HIT_RIGHT;
if (y < 0)
HitTest->Flags |= TN_HIT_ABOVE;
if (y >= clientRect.bottom)
HitTest->Flags |= TN_HIT_BELOW;
if (HitTest->Flags == 0)
{
if (TNP_HIT_TEST_FIXED_DIVIDER(x, Context))
{
HitTest->Flags |= TN_HIT_DIVIDER;
}
if (y >= Context->HeaderHeight && x < Context->FixedWidth + Context->TotalViewX)
{
index = (y - Context->HeaderHeight) / Context->RowHeight + Context->VScrollPosition;
if (index < Context->FlatList->Count)
{
HitTest->Flags |= TN_HIT_ITEM;
node = Context->FlatList->Items[index];
HitTest->Node = node;
if (HitTest->InFlags & TN_TEST_COLUMN)
{
PPH_TREENEW_COLUMN column;
LONG columnX;
column = NULL;
if (x < Context->FixedWidth && Context->FixedColumnVisible)
{
column = Context->FixedColumn;
columnX = 0;
}
else
{
LONG currentX;
ULONG i;
PPH_TREENEW_COLUMN currentColumn;
currentX = Context->NormalLeft - Context->HScrollPosition;
for (i = 0; i < Context->NumberOfColumnsByDisplay; i++)
{
currentColumn = Context->ColumnsByDisplay[i];
if (x >= currentX && x < currentX + currentColumn->Width)
{
column = currentColumn;
columnX = currentX;
break;
}
currentX += currentColumn->Width;
}
}
HitTest->Column = column;
if (column && (HitTest->InFlags & TN_TEST_SUBITEM))
{
BOOLEAN isFirstColumn;
LONG currentX;
isFirstColumn = HitTest->Column == Context->FirstColumn;
currentX = columnX;
currentX += TNP_CELL_LEFT_MARGIN;
if (isFirstColumn)
{
currentX += (LONG)node->Level * SmallIconWidth;
if (!node->s.IsLeaf)
{
if (x >= currentX && x < currentX + SmallIconWidth)
HitTest->Flags |= TN_HIT_ITEM_PLUSMINUS;
currentX += SmallIconWidth;
}
if (node->Icon)
{
if (x >= currentX && x < currentX + SmallIconWidth)
HitTest->Flags |= TN_HIT_ITEM_ICON;
currentX += SmallIconWidth + TNP_ICON_RIGHT_PADDING;
}
}
if (x >= currentX)
{
HitTest->Flags |= TN_HIT_ITEM_CONTENT;
}
}
}
}
}
}
}
VOID PhTnpSelectRange(
_In_ PPH_TREENEW_CONTEXT Context,
_In_ ULONG Start,
_In_ ULONG End,
_In_ ULONG Flags,
_Out_opt_ PULONG ChangedStart,
_Out_opt_ PULONG ChangedEnd
)
{
ULONG maximum;
ULONG i;
PPH_TREENEW_NODE node;
BOOLEAN targetValue;
ULONG changedStart;
ULONG changedEnd;
if (Context->FlatList->Count == 0)
return;
maximum = Context->FlatList->Count - 1;
if (End > maximum)
{
End = maximum;
}
if (Start > End)
{
// Start is too big, so the selection range becomes empty.
// Set it to max + 1 so that Reset still works.
Start = maximum + 1;
End = 0;
}
targetValue = !(Flags & TN_SELECT_DESELECT);
changedStart = maximum;
changedEnd = 0;
if (Flags & TN_SELECT_RESET)
{
for (i = 0; i < Start; i++)
{
node = Context->FlatList->Items[i];
if (node->Selected)
{
node->Selected = FALSE;
if (changedStart > i)
changedStart = i;
if (changedEnd < i)
changedEnd = i;
}
}
}
for (i = Start; i <= End; i++)
{
node = Context->FlatList->Items[i];
if (!node->Unselectable && ((Flags & TN_SELECT_TOGGLE) || node->Selected != targetValue))
{
node->Selected = !node->Selected;
if (changedStart > i)
changedStart = i;
if (changedEnd < i)
changedEnd = i;
}
}
if (Flags & TN_SELECT_RESET)
{
for (i = End + 1; i <= maximum; i++)
{
node = Context->FlatList->Items[i];
if (node->Selected)
{
node->Selected = FALSE;
if (changedStart > i)
changedStart = i;
if (changedEnd < i)
changedEnd = i;
}
}
}
if (changedStart <= changedEnd)
{
Context->Callback(Context->Handle, TreeNewSelectionChanged, NULL, NULL, Context->CallbackContext);
}
if (ChangedStart)
*ChangedStart = changedStart;
if (ChangedEnd)
*ChangedEnd = changedEnd;
}
VOID PhTnpSetHotNode(
_In_ PPH_TREENEW_CONTEXT Context,
_In_opt_ PPH_TREENEW_NODE NewHotNode,
_In_ BOOLEAN NewPlusMinusHot
)
{
ULONG newHotNodeIndex;
RECT rowRect;
BOOLEAN needsInvalidate;
if (NewHotNode)
newHotNodeIndex = NewHotNode->Index;
else
newHotNodeIndex = -1;
needsInvalidate = FALSE;
if (Context->HotNodeIndex != newHotNodeIndex)
{
if (Context->HotNodeIndex != -1)
{
if (Context->ThemeData && PhTnpGetRowRects(Context, Context->HotNodeIndex, Context->HotNodeIndex, TRUE, &rowRect))
{
// Update the old hot node because it may have a different non-hot background and
// plus minus part.
InvalidateRect(Context->Handle, &rowRect, FALSE);
}
}
Context->HotNodeIndex = newHotNodeIndex;
if (NewHotNode)
{
needsInvalidate = TRUE;
}
}
if (NewHotNode)
{
if (NewHotNode->s.PlusMinusHot != NewPlusMinusHot)
{
NewHotNode->s.PlusMinusHot = NewPlusMinusHot;
needsInvalidate = TRUE;
}
if (needsInvalidate && Context->ThemeData && PhTnpGetRowRects(Context, newHotNodeIndex, newHotNodeIndex, TRUE, &rowRect))
{
InvalidateRect(Context->Handle, &rowRect, FALSE);
}
}
}
VOID PhTnpProcessSelectNode(
_In_ PPH_TREENEW_CONTEXT Context,
_In_ PPH_TREENEW_NODE Node,
_In_ LOGICAL ControlKey,
_In_ LOGICAL ShiftKey,
_In_ LOGICAL RightButton
)
{
ULONG changedStart;
ULONG changedEnd;
RECT rect;
if (RightButton)
{
// Right button:
// If the current node is selected, then do nothing. This is to allow context menus to
// operate on multiple items.
// If the current node is not selected, select only that node.
if (!ControlKey && !ShiftKey && !Node->Selected)
{
PhTnpSelectRange(Context, Node->Index, Node->Index, TN_SELECT_RESET, &changedStart, &changedEnd);
Context->MarkNodeIndex = Node->Index;
if (PhTnpGetRowRects(Context, changedStart, changedEnd, TRUE, &rect))
{
InvalidateRect(Context->Handle, &rect, FALSE);
}
}
}
else if (ShiftKey && Context->MarkNodeIndex != -1)
{
ULONG start;
ULONG end;
// Shift key: select a range from the selection mark node to the current node.
if (Node->Index > Context->MarkNodeIndex)
{
start = Context->MarkNodeIndex;
end = Node->Index;
}
else
{
start = Node->Index;
end = Context->MarkNodeIndex;
}
PhTnpSelectRange(Context, start, end, TN_SELECT_RESET, &changedStart, &changedEnd);
if (PhTnpGetRowRects(Context, changedStart, changedEnd, TRUE, &rect))
{
InvalidateRect(Context->Handle, &rect, FALSE);
}
}
else if (ControlKey)
{
// Control key: toggle the selection on the current node, and also make it the selection
// mark.
PhTnpSelectRange(Context, Node->Index, Node->Index, TN_SELECT_TOGGLE, NULL, NULL);
Context->MarkNodeIndex = Node->Index;
if (PhTnpGetRowRects(Context, Node->Index, Node->Index, TRUE, &rect))
{
InvalidateRect(Context->Handle, &rect, FALSE);
}
}
else
{
// Normal: select the current node, and also make it the selection mark.
PhTnpSelectRange(Context, Node->Index, Node->Index, TN_SELECT_RESET, &changedStart, &changedEnd);
Context->MarkNodeIndex = Node->Index;
if (PhTnpGetRowRects(Context, changedStart, changedEnd, TRUE, &rect))
{
InvalidateRect(Context->Handle, &rect, FALSE);
}
}
}
BOOLEAN PhTnpEnsureVisibleNode(
_In_ PPH_TREENEW_CONTEXT Context,
_In_ ULONG Index
)
{
LONG viewTop;
LONG viewBottom;
LONG rowTop;
LONG rowBottom;
LONG deltaY;
LONG deltaRows;
if (Index >= Context->FlatList->Count)
return FALSE;
viewTop = Context->HeaderHeight;
viewBottom = Context->ClientRect.bottom - (Context->HScrollVisible ? Context->HScrollHeight : 0);
rowTop = Context->HeaderHeight + ((LONG)Index - Context->VScrollPosition) * Context->RowHeight;
rowBottom = rowTop + Context->RowHeight;
// Check if the row is fully visible.
if (rowTop >= viewTop && rowBottom <= viewBottom)
return TRUE;
deltaY = rowTop - viewTop;
if (deltaY > 0)
{
// The row is below the view area. We want to scroll the row into view at the bottom of the
// screen. We need to round up when dividing to make sure the node becomes fully visible.
deltaY = rowBottom - viewBottom;
deltaRows = (deltaY + Context->RowHeight - 1) / Context->RowHeight; // divide, round up
}
else
{
deltaRows = deltaY / Context->RowHeight;
}
PhTnpScroll(Context, deltaRows, 0);
return TRUE;
}
VOID PhTnpProcessMoveMouse(
_In_ PPH_TREENEW_CONTEXT Context,
_In_ LONG CursorX,
_In_ LONG CursorY
)
{
PH_TREENEW_HIT_TEST hitTest;
PPH_TREENEW_NODE hotNode;
hitTest.Point.x = CursorX;
hitTest.Point.y = CursorY;
hitTest.InFlags = TN_TEST_COLUMN | TN_TEST_SUBITEM;
PhTnpHitTest(Context, &hitTest);
if (hitTest.Flags & TN_HIT_ITEM)
hotNode = hitTest.Node;
else
hotNode = NULL;
PhTnpSetHotNode(Context, hotNode, !!(hitTest.Flags & TN_HIT_ITEM_PLUSMINUS));
if (Context->AnimateDivider && Context->FixedDividerVisible)
{
if (hitTest.Flags & TN_HIT_DIVIDER)
{
if ((Context->DividerHot < 100 || Context->AnimateDividerFadingOut) && !Context->AnimateDividerFadingIn)
{
// Begin fading in the divider.
Context->AnimateDividerFadingIn = TRUE;
Context->AnimateDividerFadingOut = FALSE;
SetTimer(Context->Handle, TNP_TIMER_ANIMATE_DIVIDER, TNP_ANIMATE_DIVIDER_INTERVAL, NULL);
}
}
else
{
if ((Context->DividerHot != 0 || Context->AnimateDividerFadingIn) && !Context->AnimateDividerFadingOut)
{
Context->AnimateDividerFadingOut = TRUE;
Context->AnimateDividerFadingIn = FALSE;
SetTimer(Context->Handle, TNP_TIMER_ANIMATE_DIVIDER, TNP_ANIMATE_DIVIDER_INTERVAL, NULL);
}
}
}
if (Context->TooltipsHandle)
{
ULONG index;
ULONG id;
if (!(hitTest.Flags & TN_HIT_DIVIDER))
{
index = hitTest.Node ? hitTest.Node->Index : -1;
id = hitTest.Column ? hitTest.Column->Id : -1;
}
else
{
index = -1;
id = -1;
}
// This pops unnecessarily - when the cell has no tooltip text, and the user is moving the
// mouse over it. However these unnecessary calls seem to fix a certain tooltip bug (move
// the mouse around very quickly over the last column and the blank space to the right, and
// no more tooltips will appear).
if (Context->TooltipIndex != index || Context->TooltipId != id)
{
PhTnpPopTooltip(Context);
}
}
}
VOID PhTnpProcessMouseVWheel(
_In_ PPH_TREENEW_CONTEXT Context,
_In_ LONG Distance
)
{
ULONG wheelScrollLines;
FLOAT linesToScroll;
LONG wholeLinesToScroll;
SCROLLINFO scrollInfo;
LONG oldPosition;
if (!SystemParametersInfo(SPI_GETWHEELSCROLLLINES, 0, &wheelScrollLines, 0))
wheelScrollLines = 3;
// If page scrolling is enabled, use the number of visible rows.
if (wheelScrollLines == -1)
wheelScrollLines = (Context->ClientRect.bottom - Context->HeaderHeight - (Context->HScrollVisible ? Context->HScrollHeight : 0)) / Context->RowHeight;
// Zero the remainder if the direction changed.
if ((Context->VScrollRemainder > 0) != (Distance > 0))
Context->VScrollRemainder = 0;
linesToScroll = (FLOAT)wheelScrollLines * Distance / WHEEL_DELTA + Context->VScrollRemainder;
wholeLinesToScroll = (LONG)linesToScroll;
Context->VScrollRemainder = linesToScroll - wholeLinesToScroll;
scrollInfo.cbSize = sizeof(SCROLLINFO);
scrollInfo.fMask = SIF_ALL;
GetScrollInfo(Context->VScrollHandle, SB_CTL, &scrollInfo);
oldPosition = scrollInfo.nPos;
scrollInfo.nPos += wholeLinesToScroll;
scrollInfo.fMask = SIF_POS;
SetScrollInfo(Context->VScrollHandle, SB_CTL, &scrollInfo, TRUE);
GetScrollInfo(Context->VScrollHandle, SB_CTL, &scrollInfo);
if (scrollInfo.nPos != oldPosition)
{
Context->VScrollPosition = scrollInfo.nPos;
PhTnpProcessScroll(Context, scrollInfo.nPos - oldPosition, 0);
if (Context->TooltipsHandle)
{
MSG message;
POINT point;
PhTnpPopTooltip(Context);
PhTnpGetMessagePos(Context->Handle, &point);
if (point.x >= 0 && point.y >= 0 && point.x < Context->ClientRect.right && point.y < Context->ClientRect.bottom)
{
// Send a fake mouse move message for the new node that the mouse may be hovering over.
message.hwnd = Context->Handle;
message.message = WM_MOUSEMOVE;
message.wParam = 0;
message.lParam = MAKELPARAM(point.x, point.y);
SendMessage(Context->TooltipsHandle, TTM_RELAYEVENT, 0, (LPARAM)&message);
}
}
}
}
VOID PhTnpProcessMouseHWheel(
_In_ PPH_TREENEW_CONTEXT Context,
_In_ LONG Distance
)
{
ULONG wheelScrollChars;
FLOAT pixelsToScroll;
LONG wholePixelsToScroll;
SCROLLINFO scrollInfo;
LONG oldPosition;
if (!SystemParametersInfo(SPI_GETWHEELSCROLLCHARS, 0, &wheelScrollChars, 0))
wheelScrollChars = 3;
// Zero the remainder if the direction changed.
if ((Context->HScrollRemainder > 0) != (Distance > 0))
Context->HScrollRemainder = 0;
pixelsToScroll = (FLOAT)wheelScrollChars * Context->TextMetrics.tmAveCharWidth * Distance / WHEEL_DELTA + Context->HScrollRemainder;
wholePixelsToScroll = (LONG)pixelsToScroll;
Context->HScrollRemainder = pixelsToScroll - wholePixelsToScroll;
scrollInfo.cbSize = sizeof(SCROLLINFO);
scrollInfo.fMask = SIF_ALL;
GetScrollInfo(Context->HScrollHandle, SB_CTL, &scrollInfo);
oldPosition = scrollInfo.nPos;
scrollInfo.nPos += wholePixelsToScroll;
scrollInfo.fMask = SIF_POS;
SetScrollInfo(Context->HScrollHandle, SB_CTL, &scrollInfo, TRUE);
GetScrollInfo(Context->HScrollHandle, SB_CTL, &scrollInfo);
if (scrollInfo.nPos != oldPosition)
{
Context->HScrollPosition = scrollInfo.nPos;
PhTnpProcessScroll(Context, 0, scrollInfo.nPos - oldPosition);
}
}
BOOLEAN PhTnpProcessFocusKey(
_In_ PPH_TREENEW_CONTEXT Context,
_In_ ULONG VirtualKey
)
{
ULONG count;
ULONG index;
BOOLEAN controlKey;
BOOLEAN shiftKey;
ULONG start;
ULONG end;
ULONG changedStart;
ULONG changedEnd;
RECT rect;
if (VirtualKey != VK_UP && VirtualKey != VK_DOWN &&
VirtualKey != VK_HOME && VirtualKey != VK_END &&
VirtualKey != VK_PRIOR && VirtualKey != VK_NEXT)
{
return FALSE;
}
count = Context->FlatList->Count;
if (count == 0)
return TRUE;
// Find the new node to focus.
switch (VirtualKey)
{
case VK_UP:
{
if (Context->FocusNode && Context->FocusNode->Index > 0)
{
index = Context->FocusNode->Index - 1;
}
else
{
index = 0;
}
}
break;
case VK_DOWN:
{
if (Context->FocusNode)
{
index = Context->FocusNode->Index + 1;
if (index >= count)
index = count - 1;
}
else
{
index = 0;
}
}
break;
case VK_HOME:
index = 0;
break;
case VK_END:
index = count - 1;
break;
case VK_PRIOR:
case VK_NEXT:
{
LONG rowsPerPage;
if (Context->FocusNode)
index = Context->FocusNode->Index;
else
index = 0;
rowsPerPage = Context->ClientRect.bottom - Context->HeaderHeight - (Context->HScrollVisible ? Context->HScrollHeight : 0);
if (rowsPerPage < 0)
return TRUE;
rowsPerPage = rowsPerPage / Context->RowHeight - 1;
if (rowsPerPage < 0)
return TRUE;
if (VirtualKey == VK_PRIOR)
{
ULONG startOfPageIndex;
startOfPageIndex = Context->VScrollPosition;
if (index > startOfPageIndex)
{
index = startOfPageIndex;
}
else
{
// Already at or before the start of the page. Go back a page.
if (index >= (ULONG)rowsPerPage)
index -= rowsPerPage;
else
index = 0;
}
}
else
{
ULONG endOfPageIndex;
endOfPageIndex = Context->VScrollPosition + rowsPerPage;
if (endOfPageIndex >= count)
endOfPageIndex = count - 1;
if (index < endOfPageIndex)
{
index = endOfPageIndex;
}
else
{
// Already at or after the end of the page. Go forward a page.
index += rowsPerPage;
if (index >= count)
index = count - 1;
}
}
}
break;
}
// Select the relevant nodes.
controlKey = GetKeyState(VK_CONTROL) < 0;
shiftKey = GetKeyState(VK_SHIFT) < 0;
Context->FocusNode = Context->FlatList->Items[index];
PhTnpSetHotNode(Context, Context->FocusNode, FALSE);
if (shiftKey && Context->MarkNodeIndex != -1)
{
if (index > Context->MarkNodeIndex)
{
start = Context->MarkNodeIndex;
end = index;
}
else
{
start = index;
end = Context->MarkNodeIndex;
}
PhTnpSelectRange(Context, start, end, TN_SELECT_RESET, &changedStart, &changedEnd);
if (PhTnpGetRowRects(Context, changedStart, changedEnd, TRUE, &rect))
{
InvalidateRect(Context->Handle, &rect, FALSE);
}
}
else if (!controlKey)
{
Context->MarkNodeIndex = Context->FocusNode->Index;
PhTnpSelectRange(Context, index, index, TN_SELECT_RESET, &changedStart, &changedEnd);
if (PhTnpGetRowRects(Context, changedStart, changedEnd, TRUE, &rect))
{
InvalidateRect(Context->Handle, &rect, FALSE);
}
}
PhTnpEnsureVisibleNode(Context, index);
PhTnpPopTooltip(Context);
return TRUE;
}
BOOLEAN PhTnpProcessNodeKey(
_In_ PPH_TREENEW_CONTEXT Context,
_In_ ULONG VirtualKey
)
{
BOOLEAN controlKey;
BOOLEAN shiftKey;
ULONG changedStart;
ULONG changedEnd;
RECT rect;
if (VirtualKey != VK_SPACE && VirtualKey != VK_LEFT && VirtualKey != VK_RIGHT && VirtualKey != VK_ADD && VirtualKey != VK_OEM_PLUS)
{
return FALSE;
}
if (!Context->FocusNode)
return TRUE;
controlKey = GetKeyState(VK_CONTROL) < 0;
shiftKey = GetKeyState(VK_SHIFT) < 0;
switch (VirtualKey)
{
case VK_SPACE:
{
if (controlKey)
{
// Control key: toggle the selection on the focused node.
Context->MarkNodeIndex = Context->FocusNode->Index;
PhTnpSelectRange(Context, Context->FocusNode->Index, Context->FocusNode->Index, TN_SELECT_TOGGLE, &changedStart, &changedEnd);
if (PhTnpGetRowRects(Context, changedStart, changedEnd, TRUE, &rect))
{
InvalidateRect(Context->Handle, &rect, FALSE);
}
}
else if (shiftKey)
{
ULONG start;
ULONG end;
// Shift key: select a range from the selection mark node to the focused node.
if (Context->FocusNode->Index > Context->MarkNodeIndex)
{
start = Context->MarkNodeIndex;
end = Context->FocusNode->Index;
}
else
{
start = Context->FocusNode->Index;
end = Context->MarkNodeIndex;
}
PhTnpSelectRange(Context, start, end, TN_SELECT_RESET, &changedStart, &changedEnd);
if (PhTnpGetRowRects(Context, changedStart, changedEnd, TRUE, &rect))
{
InvalidateRect(Context->Handle, &rect, FALSE);
}
}
}
break;
case VK_LEFT:
{
ULONG i;
ULONG targetLevel;
PPH_TREENEW_NODE newNode;
// If the node is expanded, collapse it. Otherwise, select the node's parent.
if (!Context->FocusNode->s.IsLeaf && Context->FocusNode->Expanded)
{
PhTnpSetExpandedNode(Context, Context->FocusNode, FALSE);
}
else if (Context->FocusNode->Level != 0)
{
i = Context->FocusNode->Index;
targetLevel = Context->FocusNode->Level - 1;
while (i != 0)
{
i--;
newNode = Context->FlatList->Items[i];
if (newNode->Level == targetLevel)
{
Context->FocusNode = newNode;
Context->MarkNodeIndex = newNode->Index;
PhTnpEnsureVisibleNode(Context, i);
PhTnpSetHotNode(Context, newNode, FALSE);
PhTnpSelectRange(Context, i, i, TN_SELECT_RESET, &changedStart, &changedEnd);
if (PhTnpGetRowRects(Context, changedStart, changedEnd, TRUE, &rect))
{
InvalidateRect(Context->Handle, &rect, FALSE);
}
PhTnpPopTooltip(Context);
break;
}
}
}
}
break;
case VK_RIGHT:
{
PPH_TREENEW_NODE newNode;
if (!Context->FocusNode->s.IsLeaf)
{
// If the node is collapsed, expand it. Otherwise, select the node's first child.
if (!Context->FocusNode->Expanded)
{
PhTnpSetExpandedNode(Context, Context->FocusNode, TRUE);
}
else
{
if (Context->FocusNode->Index + 1 < Context->FlatList->Count)
{
newNode = Context->FlatList->Items[Context->FocusNode->Index + 1];
if (newNode->Level == Context->FocusNode->Level + 1)
{
Context->FocusNode = newNode;
Context->MarkNodeIndex = newNode->Index;
PhTnpEnsureVisibleNode(Context, Context->FocusNode->Index + 1);
PhTnpSetHotNode(Context, newNode, FALSE);
PhTnpSelectRange(Context, Context->FocusNode->Index, Context->FocusNode->Index, TN_SELECT_RESET, &changedStart, &changedEnd);
if (PhTnpGetRowRects(Context, changedStart, changedEnd, TRUE, &rect))
{
InvalidateRect(Context->Handle, &rect, FALSE);
}
PhTnpPopTooltip(Context);
}
}
}
}
}
break;
case VK_ADD:
case VK_OEM_PLUS:
{
if ((VirtualKey == VK_ADD && controlKey) ||
(VirtualKey == VK_OEM_PLUS && controlKey && shiftKey))
{
ULONG i;
if (Context->FixedColumnVisible)
PhTnpAutoSizeColumnHeader(Context, Context->FixedHeaderHandle, Context->FixedColumn, 0);
for (i = 0; i < Context->NumberOfColumnsByDisplay; i++)
PhTnpAutoSizeColumnHeader(Context, Context->HeaderHandle, Context->ColumnsByDisplay[i], 0);
}
}
break;
}
return TRUE;
}
VOID PhTnpProcessSearchKey(
_In_ PPH_TREENEW_CONTEXT Context,
_In_ ULONG Character
)
{
LONG messageTime;
BOOLEAN newSearch;
PH_TREENEW_SEARCH_EVENT searchEvent;
PPH_TREENEW_NODE foundNode;
ULONG changedStart;
ULONG changedEnd;
RECT rect;
if (Context->FlatList->Count == 0)
return;
messageTime = GetMessageTime();
newSearch = FALSE;
// Check if the search timed out.
if (messageTime - Context->SearchMessageTime > PH_TREENEW_SEARCH_TIMEOUT)
{
Context->SearchStringCount = 0;
Context->SearchFailed = FALSE;
newSearch = TRUE;
Context->SearchSingleCharMode = TRUE;
}
Context->SearchMessageTime = messageTime;
// Append the character to the search buffer.
if (!Context->SearchString)
{
Context->AllocatedSearchString = 32;
Context->SearchString = PhAllocate(Context->AllocatedSearchString * sizeof(WCHAR));
newSearch = TRUE;
Context->SearchSingleCharMode = TRUE;
}
if (Context->SearchStringCount > PH_TREENEW_SEARCH_MAXIMUM_LENGTH)
{
// The search string has become too long. Fail the search.
if (!Context->SearchFailed)
{
MessageBeep(0);
Context->SearchFailed = TRUE;
return;
}
}
else if (Context->SearchStringCount == Context->AllocatedSearchString)
{
Context->AllocatedSearchString *= 2;
Context->SearchString = PhReAllocate(Context->SearchString, Context->AllocatedSearchString * sizeof(WCHAR));
}
Context->SearchString[Context->SearchStringCount++] = (WCHAR)Character;
if (Context->SearchString[Context->SearchStringCount - 1] != Context->SearchString[0])
{
// The user has stopped typing the same character (or never started). Turn single-character
// search off.
Context->SearchSingleCharMode = FALSE;
}
searchEvent.FoundIndex = -1;
if (Context->FocusNode)
{
searchEvent.StartIndex = Context->FocusNode->Index;
if (newSearch || Context->SearchSingleCharMode)
{
// If it's a new search, start at the next item so the user doesn't find the same item
// again.
searchEvent.StartIndex++;
if (searchEvent.StartIndex == Context->FlatList->Count)
searchEvent.StartIndex = 0;
}
}
else
{
searchEvent.StartIndex = 0;
}
searchEvent.String.Buffer = Context->SearchString;
if (!Context->SearchSingleCharMode)
searchEvent.String.Length = Context->SearchStringCount * sizeof(WCHAR);
else
searchEvent.String.Length = sizeof(WCHAR);
// Give the user a chance to modify how the search is performed.
if (!Context->Callback(Context->Handle, TreeNewIncrementalSearch, &searchEvent, NULL, Context->CallbackContext))
{
// Use the default search function.
if (!PhTnpDefaultIncrementalSearch(Context, &searchEvent, TRUE, TRUE))
{
return;
}
}
if (searchEvent.FoundIndex == -1 && !Context->SearchFailed)
{
// No search result. Beep to indicate an error, and set the flag so we don't beep again. But
// don't beep if the first character was a space, because that's used for other purposes
// elsewhere (see PhTnpProcessNodeKey).
if (searchEvent.String.Buffer[0] != ' ')
{
MessageBeep(0);
}
Context->SearchFailed = TRUE;
return;
}
if (searchEvent.FoundIndex < 0 || searchEvent.FoundIndex >= (LONG)Context->FlatList->Count)
return;
foundNode = Context->FlatList->Items[searchEvent.FoundIndex];
Context->FocusNode = foundNode;
PhTnpEnsureVisibleNode(Context, searchEvent.FoundIndex);
PhTnpSetHotNode(Context, foundNode, FALSE);
PhTnpSelectRange(Context, searchEvent.FoundIndex, searchEvent.FoundIndex, TN_SELECT_RESET, &changedStart, &changedEnd);
if (PhTnpGetRowRects(Context, changedStart, changedEnd, TRUE, &rect))
{
InvalidateRect(Context->Handle, &rect, FALSE);
}
PhTnpPopTooltip(Context);
}
BOOLEAN PhTnpDefaultIncrementalSearch(
_In_ PPH_TREENEW_CONTEXT Context,
_Inout_ PPH_TREENEW_SEARCH_EVENT SearchEvent,
_In_ BOOLEAN Partial,
_In_ BOOLEAN Wrap
)
{
LONG startIndex;
LONG currentIndex;
LONG foundIndex;
BOOLEAN firstTime;
if (Context->FlatList->Count == 0)
return FALSE;
if (!Context->FirstColumn)
return FALSE;
startIndex = SearchEvent->StartIndex;
currentIndex = startIndex;
foundIndex = -1;
firstTime = TRUE;
while (TRUE)
{
PH_STRINGREF text;
if (currentIndex >= (LONG)Context->FlatList->Count)
{
if (Wrap)
currentIndex = 0;
else
break;
}
// We use the firstTime variable instead of a simpler check because we want to include the
// current item in the search. E.g. the current item is the only item beginning with "Z". If
// the user searches for "Z", we want to return the current item as being found.
if (!firstTime && currentIndex == startIndex)
break;
if (PhTnpGetCellText(Context, Context->FlatList->Items[currentIndex], Context->FirstColumn->Id, &text))
{
if (Partial)
{
if (PhStartsWithStringRef(&text, &SearchEvent->String, TRUE))
{
foundIndex = currentIndex;
break;
}
}
else
{
if (PhEqualStringRef(&text, &SearchEvent->String, TRUE))
{
foundIndex = currentIndex;
break;
}
}
}
currentIndex++;
firstTime = FALSE;
}
SearchEvent->FoundIndex = foundIndex;
return TRUE;
}
VOID PhTnpUpdateScrollBars(
_In_ PPH_TREENEW_CONTEXT Context
)
{
RECT clientRect;
LONG width;
LONG height;
LONG contentWidth;
LONG contentHeight;
SCROLLINFO scrollInfo;
LONG oldPosition;
LONG deltaRows;
LONG deltaX;
LOGICAL oldHScrollVisible;
RECT rect;
clientRect = Context->ClientRect;
width = clientRect.right - Context->FixedWidth;
height = clientRect.bottom - Context->HeaderHeight;
contentWidth = Context->TotalViewX;
contentHeight = (LONG)Context->FlatList->Count * Context->RowHeight;
if (contentHeight > height)
{
// We need a vertical scrollbar, so we can't use that area of the screen for content.
width -= Context->VScrollWidth;
}
if (contentWidth > width)
{
height -= Context->HScrollHeight;
}
deltaRows = 0;
deltaX = 0;
// Vertical scroll bar
scrollInfo.cbSize = sizeof(SCROLLINFO);
scrollInfo.fMask = SIF_POS;
GetScrollInfo(Context->VScrollHandle, SB_CTL, &scrollInfo);
oldPosition = scrollInfo.nPos;
scrollInfo.fMask = SIF_RANGE | SIF_PAGE;
scrollInfo.nMin = 0;
scrollInfo.nMax = Context->FlatList->Count != 0 ? Context->FlatList->Count - 1 : 0;
scrollInfo.nPage = height / Context->RowHeight;
SetScrollInfo(Context->VScrollHandle, SB_CTL, &scrollInfo, TRUE);
// The scroll position may have changed due to the modified scroll range.
scrollInfo.fMask = SIF_POS;
GetScrollInfo(Context->VScrollHandle, SB_CTL, &scrollInfo);
deltaRows = scrollInfo.nPos - oldPosition;
Context->VScrollPosition = scrollInfo.nPos;
if (contentHeight > height && contentHeight != 0)
{
ShowWindow(Context->VScrollHandle, SW_SHOW);
Context->VScrollVisible = TRUE;
}
else
{
ShowWindow(Context->VScrollHandle, SW_HIDE);
Context->VScrollVisible = FALSE;
}
// Horizontal scroll bar
scrollInfo.cbSize = sizeof(SCROLLINFO);
scrollInfo.fMask = SIF_POS;
GetScrollInfo(Context->HScrollHandle, SB_CTL, &scrollInfo);
oldPosition = scrollInfo.nPos;
scrollInfo.fMask = SIF_RANGE | SIF_PAGE;
scrollInfo.nMin = 0;
scrollInfo.nMax = contentWidth != 0 ? contentWidth - 1 : 0;
scrollInfo.nPage = width;
SetScrollInfo(Context->HScrollHandle, SB_CTL, &scrollInfo, TRUE);
scrollInfo.fMask = SIF_POS;
GetScrollInfo(Context->HScrollHandle, SB_CTL, &scrollInfo);
deltaX = scrollInfo.nPos - oldPosition;
Context->HScrollPosition = scrollInfo.nPos;
oldHScrollVisible = Context->HScrollVisible;
if (contentWidth > width && contentWidth != 0)
{
ShowWindow(Context->HScrollHandle, SW_SHOW);
Context->HScrollVisible = TRUE;
}
else
{
ShowWindow(Context->HScrollHandle, SW_HIDE);
Context->HScrollVisible = FALSE;
}
if ((Context->HScrollVisible != oldHScrollVisible) && Context->FixedDividerVisible && Context->AnimateDivider)
{
rect.left = Context->FixedWidth;
rect.top = Context->HeaderHeight;
rect.right = Context->FixedWidth + 1;
rect.bottom = Context->ClientRect.bottom;
InvalidateRect(Context->Handle, &rect, FALSE);
}
if (deltaRows != 0 || deltaX != 0)
PhTnpProcessScroll(Context, deltaRows, deltaX);
ShowWindow(Context->FillerBoxHandle, (Context->VScrollVisible && Context->HScrollVisible) ? SW_SHOW : SW_HIDE);
}
VOID PhTnpScroll(
_In_ PPH_TREENEW_CONTEXT Context,
_In_ LONG DeltaRows,
_In_ LONG DeltaX
)
{
SCROLLINFO scrollInfo;
LONG oldPosition;
LONG deltaRows;
LONG deltaX;
deltaRows = 0;
deltaX = 0;
scrollInfo.cbSize = sizeof(SCROLLINFO);
scrollInfo.fMask = SIF_POS;
if (DeltaRows != 0 && Context->VScrollVisible)
{
GetScrollInfo(Context->VScrollHandle, SB_CTL, &scrollInfo);
oldPosition = scrollInfo.nPos;
if (DeltaRows == MINLONG)
scrollInfo.nPos = 0;
else if (DeltaRows == MAXLONG)
scrollInfo.nPos = Context->FlatList->Count - 1;
else
scrollInfo.nPos += DeltaRows;
SetScrollInfo(Context->VScrollHandle, SB_CTL, &scrollInfo, TRUE);
GetScrollInfo(Context->VScrollHandle, SB_CTL, &scrollInfo);
Context->VScrollPosition = scrollInfo.nPos;
deltaRows = scrollInfo.nPos - oldPosition;
}
if (DeltaX != 0 && Context->HScrollVisible)
{
GetScrollInfo(Context->HScrollHandle, SB_CTL, &scrollInfo);
oldPosition = scrollInfo.nPos;
if (DeltaX == MINLONG)
scrollInfo.nPos = 0;
else if (DeltaX == MAXLONG)
scrollInfo.nPos = Context->TotalViewX;
else
scrollInfo.nPos += DeltaX;
SetScrollInfo(Context->HScrollHandle, SB_CTL, &scrollInfo, TRUE);
GetScrollInfo(Context->HScrollHandle, SB_CTL, &scrollInfo);
Context->HScrollPosition = scrollInfo.nPos;
deltaX = scrollInfo.nPos - oldPosition;
}
if (deltaRows != 0 || deltaX != 0)
PhTnpProcessScroll(Context, deltaRows, deltaX);
}
VOID PhTnpProcessScroll(
_In_ PPH_TREENEW_CONTEXT Context,
_In_ LONG DeltaRows,
_In_ LONG DeltaX
)
{
RECT rect;
LONG deltaY;
rect.top = Context->HeaderHeight;
rect.bottom = Context->ClientRect.bottom;
if (DeltaX == 0)
{
rect.left = 0;
rect.right = Context->ClientRect.right - (Context->VScrollVisible ? Context->VScrollWidth : 0);
ScrollWindowEx(
Context->Handle,
0,
-DeltaRows * Context->RowHeight,
&rect,
NULL,
NULL,
NULL,
SW_INVALIDATE
);
}
else
{
// Don't scroll if there are no rows. This is especially important if the user wants us to
// display empty text.
if (Context->FlatList->Count != 0)
{
deltaY = DeltaRows * Context->RowHeight;
// If we're scrolling vertically as well, we need to scroll the fixed part and the
// normal part separately.
if (DeltaRows != 0)
{
rect.left = 0;
rect.right = Context->NormalLeft;
ScrollWindowEx(
Context->Handle,
0,
-deltaY,
&rect,
&rect,
NULL,
NULL,
SW_INVALIDATE
);
}
rect.left = Context->NormalLeft;
rect.right = Context->ClientRect.right - (Context->VScrollVisible ? Context->VScrollWidth : 0);
ScrollWindowEx(
Context->Handle,
-DeltaX,
-deltaY,
&rect,
&rect,
NULL,
NULL,
SW_INVALIDATE
);
}
PhTnpLayoutHeader(Context);
}
}
BOOLEAN PhTnpCanScroll(
_In_ PPH_TREENEW_CONTEXT Context,
_In_ BOOLEAN Horizontal,
_In_ BOOLEAN Positive
)
{
SCROLLINFO scrollInfo;
scrollInfo.cbSize = sizeof(SCROLLINFO);
scrollInfo.fMask = SIF_RANGE | SIF_PAGE | SIF_POS;
if (!Horizontal)
GetScrollInfo(Context->VScrollHandle, SB_CTL, &scrollInfo);
else
GetScrollInfo(Context->HScrollHandle, SB_CTL, &scrollInfo);
if (Positive)
{
if (scrollInfo.nPage != 0)
scrollInfo.nMax -= scrollInfo.nPage - 1;
return scrollInfo.nPos < scrollInfo.nMax;
}
else
{
return scrollInfo.nPos > scrollInfo.nMin;
}
}
VOID PhTnpPaint(
_In_ HWND hwnd,
_In_ PPH_TREENEW_CONTEXT Context,
_In_ HDC hdc,
_In_ PRECT PaintRect
)
{
RECT viewRect;
LONG vScrollPosition;
LONG hScrollPosition;
LONG firstRowToUpdate;
LONG lastRowToUpdate;
LONG i;
LONG j;
PPH_TREENEW_NODE node;
PPH_TREENEW_COLUMN column;
RECT rowRect;
LONG x;
BOOLEAN fixedUpdate;
LONG normalUpdateLeftX;
LONG normalUpdateRightX;
LONG normalUpdateLeftIndex;
LONG normalUpdateRightIndex;
LONG normalTotalX;
RECT cellRect;
HBRUSH backBrush;
HRGN oldClipRegion;
PhTnpInitializeThemeData(Context);
viewRect = Context->ClientRect;
if (Context->VScrollVisible)
viewRect.right -= Context->VScrollWidth;
vScrollPosition = Context->VScrollPosition;
hScrollPosition = Context->HScrollPosition;
// Calculate the indicies of the first and last rows that need painting. These indicies are
// relative to the top of the view area.
firstRowToUpdate = (PaintRect->top - Context->HeaderHeight) / Context->RowHeight;
lastRowToUpdate = (PaintRect->bottom - 1 - Context->HeaderHeight) / Context->RowHeight; // minus one since bottom is exclusive
if (firstRowToUpdate < 0)
firstRowToUpdate = 0;
rowRect.left = 0;
rowRect.top = Context->HeaderHeight + firstRowToUpdate * Context->RowHeight;
rowRect.right = Context->NormalLeft + Context->TotalViewX - Context->HScrollPosition;
rowRect.bottom = rowRect.top + Context->RowHeight;
// Change the indicies to absolute row indicies.
firstRowToUpdate += vScrollPosition;
lastRowToUpdate += vScrollPosition;
if (lastRowToUpdate >= (LONG)Context->FlatList->Count)
lastRowToUpdate = Context->FlatList->Count - 1; // becomes -1 when there are no items, handled correctly by loop below
// Determine whether the fixed column needs painting, and which normal columns need painting.
fixedUpdate = FALSE;
if (Context->FixedColumnVisible && PaintRect->left < Context->FixedWidth)
fixedUpdate = TRUE;
x = Context->NormalLeft - hScrollPosition;
normalUpdateLeftX = viewRect.right;
normalUpdateLeftIndex = 0;
normalUpdateRightX = 0;
normalUpdateRightIndex = -1;
for (j = 0; j < (LONG)Context->NumberOfColumnsByDisplay; j++)
{
column = Context->ColumnsByDisplay[j];
if (x + column->Width >= Context->NormalLeft && x + column->Width > PaintRect->left && x < PaintRect->right)
{
if (normalUpdateLeftX > x)
{
normalUpdateLeftX = x;
normalUpdateLeftIndex = j;
}
if (normalUpdateRightX < x + column->Width)
{
normalUpdateRightX = x + column->Width;
normalUpdateRightIndex = j;
}
}
x += column->Width;
}
normalTotalX = x;
if (normalUpdateRightIndex >= (LONG)Context->NumberOfColumnsByDisplay)
normalUpdateRightIndex = Context->NumberOfColumnsByDisplay - 1;
// Paint the rows.
SelectObject(hdc, Context->Font);
SetBkMode(hdc, TRANSPARENT);
for (i = firstRowToUpdate; i <= lastRowToUpdate; i++)
{
node = Context->FlatList->Items[i];
// Prepare the row for drawing.
PhTnpPrepareRowForDraw(Context, hdc, node);
if (node->Selected && !Context->ThemeHasItemBackground)
{
// Non-themed background
if (Context->HasFocus)
{
SetTextColor(hdc, GetSysColor(COLOR_HIGHLIGHTTEXT));
backBrush = GetSysColorBrush(COLOR_HIGHLIGHT);
}
else
{
SetTextColor(hdc, GetSysColor(COLOR_BTNTEXT));
backBrush = GetSysColorBrush(COLOR_BTNFACE);
}
}
else
{
SetTextColor(hdc, node->s.DrawForeColor);
SetDCBrushColor(hdc, node->s.DrawBackColor);
backBrush = GetStockObject(DC_BRUSH);
}
FillRect(hdc, &rowRect, backBrush);
if (Context->ThemeHasItemBackground)
{
INT stateId;
// Themed background
if (node->Selected)
{
if (i == Context->HotNodeIndex)
stateId = TREIS_HOTSELECTED;
else if (!Context->HasFocus)
stateId = TREIS_SELECTEDNOTFOCUS;
else
stateId = TREIS_SELECTED;
}
else
{
if (i == Context->HotNodeIndex)
stateId = TREIS_HOT;
else
stateId = -1;
}
if (stateId != -1)
{
if (!Context->FixedColumnVisible)
{
rowRect.left = Context->NormalLeft - hScrollPosition;
}
DrawThemeBackground(
Context->ThemeData,
hdc,
TVP_TREEITEM,
stateId,
&rowRect,
PaintRect
);
}
}
// Paint the fixed column.
cellRect.top = rowRect.top;
cellRect.bottom = rowRect.bottom;
if (fixedUpdate)
{
cellRect.left = 0;
cellRect.right = Context->FixedWidth;
PhTnpDrawCell(Context, hdc, &cellRect, node, Context->FixedColumn, i, -1);
}
// Paint the normal columns.
if (normalUpdateLeftX < normalUpdateRightX)
{
cellRect.left = normalUpdateLeftX;
cellRect.right = cellRect.left;
oldClipRegion = CreateRectRgn(0, 0, 0, 0);
if (GetClipRgn(hdc, oldClipRegion) != 1)
{
DeleteObject(oldClipRegion);
oldClipRegion = NULL;
}
IntersectClipRect(hdc, Context->NormalLeft, cellRect.top, viewRect.right, cellRect.bottom);
for (j = normalUpdateLeftIndex; j <= normalUpdateRightIndex; j++)
{
column = Context->ColumnsByDisplay[j];
cellRect.left = cellRect.right;
cellRect.right = cellRect.left + column->Width;
PhTnpDrawCell(Context, hdc, &cellRect, node, column, i, j);
}
SelectClipRgn(hdc, oldClipRegion);
if (oldClipRegion)
{
DeleteObject(oldClipRegion);
}
}
rowRect.top += Context->RowHeight;
rowRect.bottom += Context->RowHeight;
}
if (lastRowToUpdate == Context->FlatList->Count - 1) // works even if there are no items
{
// Fill the rest of the space on the bottom with the window color.
rowRect.bottom = viewRect.bottom;
FillRect(hdc, &rowRect, GetSysColorBrush(COLOR_WINDOW));
}
if (normalTotalX < viewRect.right && viewRect.right > PaintRect->left && normalTotalX < PaintRect->right)
{
// Fill the rest of the space on the right with the window color.
rowRect.left = normalTotalX;
rowRect.top = Context->HeaderHeight;
rowRect.right = viewRect.right;
rowRect.bottom = viewRect.bottom;
FillRect(hdc, &rowRect, GetSysColorBrush(COLOR_WINDOW));
}
if (Context->FlatList->Count == 0 && Context->EmptyText.Length != 0)
{
RECT textRect;
textRect.left = 20;
textRect.top = Context->HeaderHeight + 10;
textRect.right = viewRect.right - 20;
textRect.bottom = viewRect.bottom - 5;
SetTextColor(hdc, GetSysColor(COLOR_GRAYTEXT));
DrawText(
hdc,
Context->EmptyText.Buffer,
(ULONG)Context->EmptyText.Length / 2,
&textRect,
DT_NOPREFIX | DT_CENTER | DT_END_ELLIPSIS
);
}
if (Context->FixedDividerVisible && Context->FixedWidth >= PaintRect->left && Context->FixedWidth < PaintRect->right)
{
PhTnpDrawDivider(Context, hdc);
}
if (Context->DragSelectionActive)
{
PhTnpDrawSelectionRectangle(Context, hdc, &Context->DragRect);
}
}
VOID PhTnpPrepareRowForDraw(
_In_ PPH_TREENEW_CONTEXT Context,
_In_ HDC hdc,
_Inout_ PPH_TREENEW_NODE Node
)
{
if (!Node->s.CachedColorValid)
{
PH_TREENEW_GET_NODE_COLOR getNodeColor;
getNodeColor.Flags = 0;
getNodeColor.Node = Node;
getNodeColor.BackColor = RGB(0xff, 0xff, 0xff);
getNodeColor.ForeColor = RGB(0x00, 0x00, 0x00);
if (Context->Callback(
Context->Handle,
TreeNewGetNodeColor,
&getNodeColor,
NULL,
Context->CallbackContext
))
{
Node->BackColor = getNodeColor.BackColor;
Node->ForeColor = getNodeColor.ForeColor;
Node->UseAutoForeColor = !!(getNodeColor.Flags & TN_AUTO_FORECOLOR);
if (getNodeColor.Flags & TN_CACHE)
Node->s.CachedColorValid = TRUE;
}
else
{
Node->BackColor = getNodeColor.BackColor;
Node->ForeColor = getNodeColor.ForeColor;
}
}
Node->s.DrawForeColor = Node->ForeColor;
if (Node->UseTempBackColor)
Node->s.DrawBackColor = Node->TempBackColor;
else
Node->s.DrawBackColor = Node->BackColor;
if (!Node->s.CachedFontValid)
{
PH_TREENEW_GET_NODE_FONT getNodeFont;
getNodeFont.Flags = 0;
getNodeFont.Node = Node;
getNodeFont.Font = NULL;
if (Context->Callback(
Context->Handle,
TreeNewGetNodeFont,
&getNodeFont,
NULL,
Context->CallbackContext
))
{
Node->Font = getNodeFont.Font;
if (getNodeFont.Flags & TN_CACHE)
Node->s.CachedFontValid = TRUE;
}
else
{
Node->Font = NULL;
}
}
if (!Node->s.CachedIconValid)
{
PH_TREENEW_GET_NODE_ICON getNodeIcon;
getNodeIcon.Flags = 0;
getNodeIcon.Node = Node;
getNodeIcon.Icon = NULL;
if (Context->Callback(
Context->Handle,
TreeNewGetNodeIcon,
&getNodeIcon,
NULL,
Context->CallbackContext
))
{
Node->Icon = getNodeIcon.Icon;
if (getNodeIcon.Flags & TN_CACHE)
Node->s.CachedIconValid = TRUE;
}
else
{
Node->Icon = NULL;
}
}
if (Node->UseAutoForeColor || Node->UseTempBackColor)
{
if (PhGetColorBrightness(Node->s.DrawBackColor) > 100) // slightly less than half
Node->s.DrawForeColor = RGB(0x00, 0x00, 0x00);
else
Node->s.DrawForeColor = RGB(0xff, 0xff, 0xff);
}
}
VOID PhTnpDrawCell(
_In_ PPH_TREENEW_CONTEXT Context,
_In_ HDC hdc,
_In_ PRECT CellRect,
_In_ PPH_TREENEW_NODE Node,
_In_ PPH_TREENEW_COLUMN Column,
_In_ LONG RowIndex,
_In_ LONG ColumnIndex
)
{
HFONT font; // font to use
HFONT oldFont;
PH_STRINGREF text; // text to draw
RECT textRect; // working rectangle, modified as needed
ULONG textFlags; // DT_* flags
LONG iconVerticalMargin; // top/bottom margin for icons (determined using height of small icon)
font = Node->Font;
textFlags = Column->TextFlags;
textRect = *CellRect;
// Initial margins used by default list view
textRect.left += TNP_CELL_LEFT_MARGIN;
textRect.right -= TNP_CELL_RIGHT_MARGIN;
// icon margin = (height of row - height of small icon) / 2
iconVerticalMargin = ((textRect.bottom - textRect.top) - SmallIconHeight) / 2;
textRect.top += iconVerticalMargin;
textRect.bottom -= iconVerticalMargin;
if (Column == Context->FirstColumn)
{
BOOLEAN needsClip;
HRGN oldClipRegion;
textRect.left += Node->Level * SmallIconWidth;
// The icon may need to be clipped if the column is too small.
needsClip = Column->Width < textRect.left + (Context->CanAnyExpand ? SmallIconWidth : 0) + (Node->Icon ? SmallIconWidth : 0);
if (needsClip)
{
oldClipRegion = CreateRectRgn(0, 0, 0, 0);
if (GetClipRgn(hdc, oldClipRegion) != 1)
{
DeleteObject(oldClipRegion);
oldClipRegion = NULL;
}
// Clip contents to the column.
IntersectClipRect(hdc, CellRect->left, textRect.top, CellRect->right, textRect.bottom);
}
if (Context->CanAnyExpand) // flag is used so we can avoid indenting when it's a flat list
{
BOOLEAN drewUsingTheme = FALSE;
RECT themeRect;
if (!Node->s.IsLeaf)
{
// Draw the plus/minus glyph.
themeRect.left = textRect.left;
themeRect.right = themeRect.left + SmallIconWidth;
themeRect.top = textRect.top;
themeRect.bottom = themeRect.top + SmallIconHeight;
if (Context->ThemeHasGlyph)
{
INT partId;
INT stateId;
partId = (RowIndex == Context->HotNodeIndex && Node->s.PlusMinusHot && Context->ThemeHasHotGlyph) ? TVP_HOTGLYPH : TVP_GLYPH;
stateId = Node->Expanded ? GLPS_OPENED : GLPS_CLOSED;
if (SUCCEEDED(DrawThemeBackground(
Context->ThemeData,
hdc,
partId,
stateId,
&themeRect,
NULL
)))
drewUsingTheme = TRUE;
}
if (!drewUsingTheme)
{
ULONG glyphWidth;
ULONG glyphHeight;
RECT glyphRect;
glyphWidth = SmallIconWidth / 2;
glyphHeight = SmallIconHeight / 2;
glyphRect.left = textRect.left + (SmallIconWidth - glyphWidth) / 2;
glyphRect.right = glyphRect.left + glyphWidth;
glyphRect.top = textRect.top + (SmallIconHeight - glyphHeight) / 2;
glyphRect.bottom = glyphRect.top + glyphHeight;
PhTnpDrawPlusMinusGlyph(hdc, &glyphRect, !Node->Expanded);
}
}
textRect.left += SmallIconWidth;
}
// Draw the icon.
if (Node->Icon)
{
DrawIconEx(
hdc,
textRect.left,
textRect.top,
Node->Icon,
SmallIconWidth,
SmallIconHeight,
0,
NULL,
DI_NORMAL
);
textRect.left += SmallIconWidth + TNP_ICON_RIGHT_PADDING;
}
if (needsClip)
{
SelectClipRgn(hdc, oldClipRegion);
if (oldClipRegion)
DeleteObject(oldClipRegion);
}
if (textRect.left > textRect.right)
textRect.left = textRect.right;
}
if (Column->CustomDraw)
{
BOOLEAN result;
PH_TREENEW_CUSTOM_DRAW customDraw;
INT savedDc;
customDraw.Node = Node;
customDraw.Column = Column;
customDraw.Dc = hdc;
customDraw.CellRect = *CellRect;
customDraw.TextRect = textRect;
// Fix up the rectangles before giving them to the user.
if (customDraw.CellRect.left > customDraw.CellRect.right)
customDraw.CellRect.left = customDraw.CellRect.right;
if (customDraw.TextRect.left > customDraw.TextRect.right)
customDraw.TextRect.left = customDraw.TextRect.right;
savedDc = SaveDC(hdc);
result = Context->Callback(Context->Handle, TreeNewCustomDraw, &customDraw, NULL, Context->CallbackContext);
RestoreDC(hdc, savedDc);
if (result)
return;
}
if (PhTnpGetCellText(Context, Node, Column->Id, &text))
{
if (!(textFlags & (DT_PATH_ELLIPSIS | DT_WORD_ELLIPSIS)))
textFlags |= DT_END_ELLIPSIS;
textFlags |= DT_NOPREFIX | DT_VCENTER | DT_SINGLELINE;
textRect.top = CellRect->top;
textRect.bottom = CellRect->bottom;
if (font)
oldFont = SelectObject(hdc, font);
DrawText(
hdc,
text.Buffer,
(ULONG)text.Length / 2,
&textRect,
textFlags
);
if (font)
SelectObject(hdc, oldFont);
}
}
VOID PhTnpDrawDivider(
_In_ PPH_TREENEW_CONTEXT Context,
_In_ HDC hdc
)
{
POINT points[2];
if (Context->AnimateDivider)
{
if (Context->DividerHot == 0 && !Context->HScrollVisible)
return; // divider is invisible
if (Context->DividerHot < 100)
{
BLENDFUNCTION blendFunction;
// We need to draw and alpha blend the divider.
// We can use the extra column allocated in the buffered context to initially draw the
// divider.
points[0].x = Context->ClientRect.right;
points[0].y = Context->HeaderHeight;
points[1].x = Context->ClientRect.right;
points[1].y = Context->ClientRect.bottom;
SetDCPenColor(Context->BufferedContext, RGB(0x77, 0x77, 0x77));
SelectObject(Context->BufferedContext, GetStockObject(DC_PEN));
Polyline(Context->BufferedContext, points, 2);
blendFunction.BlendOp = AC_SRC_OVER;
blendFunction.BlendFlags = 0;
blendFunction.AlphaFormat = 0;
// If the horizontal scroll bar is visible, we need to display a line even if the
// divider is not hot. In this case we increase the base alpha value.
if (!Context->HScrollVisible)
blendFunction.SourceConstantAlpha = (UCHAR)(Context->DividerHot * 255 / 100);
else
blendFunction.SourceConstantAlpha = 55 + (UCHAR)(Context->DividerHot * 2);
GdiAlphaBlend(
hdc,
Context->FixedWidth,
Context->HeaderHeight,
1,
Context->ClientRect.bottom - Context->HeaderHeight,
Context->BufferedContext,
Context->ClientRect.right,
Context->HeaderHeight,
1,
Context->ClientRect.bottom - Context->HeaderHeight,
blendFunction
);
return;
}
}
points[0].x = Context->FixedWidth;
points[0].y = Context->HeaderHeight;
points[1].x = Context->FixedWidth;
points[1].y = Context->ClientRect.bottom;
SetDCPenColor(hdc, RGB(0x77, 0x77, 0x77));
SelectObject(hdc, GetStockObject(DC_PEN));
Polyline(hdc, points, 2);
}
VOID PhTnpDrawPlusMinusGlyph(
_In_ HDC hdc,
_In_ PRECT Rect,
_In_ BOOLEAN Plus
)
{
INT savedDc;
ULONG width;
ULONG height;
POINT points[2];
savedDc = SaveDC(hdc);
SelectObject(hdc, GetStockObject(DC_PEN));
SetDCPenColor(hdc, RGB(0x55, 0x55, 0x55));
SelectObject(hdc, GetStockObject(DC_BRUSH));
SetDCBrushColor(hdc, RGB(0xff, 0xff, 0xff));
width = Rect->right - Rect->left;
height = Rect->bottom - Rect->top;
// Draw the rectangle.
Rectangle(hdc, Rect->left, Rect->top, Rect->right + 1, Rect->bottom + 1);
SetDCPenColor(hdc, RGB(0x00, 0x00, 0x00));
// Draw the horizontal line.
points[0].x = Rect->left + 2;
points[0].y = Rect->top + height / 2;
points[1].x = Rect->right - 2 + 1;
points[1].y = points[0].y;
Polyline(hdc, points, 2);
if (Plus)
{
// Draw the vertical line.
points[0].x = Rect->left + width / 2;
points[0].y = Rect->top + 2;
points[1].x = points[0].x;
points[1].y = Rect->bottom - 2 + 1;
Polyline(hdc, points, 2);
}
RestoreDC(hdc, savedDc);
}
VOID PhTnpDrawSelectionRectangle(
_In_ PPH_TREENEW_CONTEXT Context,
_In_ HDC hdc,
_In_ PRECT Rect
)
{
RECT rect;
BOOLEAN drewWithAlpha;
rect = *Rect;
// MSDN says FrameRect/DrawFocusRect doesn't draw anything if bottom <= top or right <= left.
// That's complete rubbish.
if (rect.right - rect.left == 0 || rect.bottom - rect.top == 0)
return;
drewWithAlpha = FALSE;
if (Context->SelectionRectangleAlpha)
{
HDC tempDc;
BITMAPINFOHEADER header;
HBITMAP bitmap;
HBITMAP oldBitmap;
PVOID bits;
RECT tempRect;
BLENDFUNCTION blendFunction;
tempDc = CreateCompatibleDC(hdc);
if (tempDc)
{
memset(&header, 0, sizeof(BITMAPINFOHEADER));
header.biSize = sizeof(BITMAPINFOHEADER);
header.biWidth = 1;
header.biHeight = 1;
header.biPlanes = 1;
header.biBitCount = 24;
bitmap = CreateDIBSection(tempDc, (BITMAPINFO *)&header, DIB_RGB_COLORS, &bits, NULL, 0);
if (bitmap)
{
// Draw the outline of the selection rectangle.
FrameRect(hdc, &rect, GetSysColorBrush(COLOR_HIGHLIGHT));
// Fill in the selection rectangle.
oldBitmap = SelectObject(tempDc, bitmap);
tempRect.left = 0;
tempRect.top = 0;
tempRect.right = 1;
tempRect.bottom = 1;
FillRect(tempDc, &tempRect, GetSysColorBrush(COLOR_HOTLIGHT));
blendFunction.BlendOp = AC_SRC_OVER;
blendFunction.BlendFlags = 0;
blendFunction.SourceConstantAlpha = 70;
blendFunction.AlphaFormat = 0;
GdiAlphaBlend(
hdc,
rect.left,
rect.top,
rect.right - rect.left,
rect.bottom - rect.top,
tempDc,
0,
0,
1,
1,
blendFunction
);
drewWithAlpha = TRUE;
SelectObject(tempDc, oldBitmap);
DeleteObject(bitmap);
}
DeleteDC(tempDc);
}
}
if (!drewWithAlpha)
{
DrawFocusRect(hdc, &rect);
}
}
VOID PhTnpDrawThemedBorder(
_In_ PPH_TREENEW_CONTEXT Context,
_In_ HDC hdc
)
{
RECT windowRect;
RECT clientRect;
LONG sizingBorderWidth;
LONG borderX;
LONG borderY;
GetWindowRect(Context->Handle, &windowRect);
windowRect.right -= windowRect.left;
windowRect.bottom -= windowRect.top;
windowRect.left = 0;
windowRect.top = 0;
clientRect.left = windowRect.left + Context->SystemEdgeX;
clientRect.top = windowRect.top + Context->SystemEdgeY;
clientRect.right = windowRect.right - Context->SystemEdgeX;
clientRect.bottom = windowRect.bottom - Context->SystemEdgeY;
// Make sure we don't paint in the client area.
ExcludeClipRect(hdc, clientRect.left, clientRect.top, clientRect.right, clientRect.bottom);
// Draw the themed border.
DrawThemeBackground(Context->ThemeData, hdc, 0, 0, &windowRect, NULL);
// Calculate the size of the border we just drew, and fill in the rest of the space if we didn't
// fully paint the region.
if (SUCCEEDED(GetThemeInt(Context->ThemeData, 0, 0, TMT_SIZINGBORDERWIDTH, &sizingBorderWidth)))
{
borderX = sizingBorderWidth;
borderY = sizingBorderWidth;
}
else
{
borderX = Context->SystemBorderX;
borderY = Context->SystemBorderY;
}
if (borderX < Context->SystemEdgeX || borderY < Context->SystemEdgeY)
{
windowRect.left += Context->SystemEdgeX - borderX;
windowRect.top += Context->SystemEdgeY - borderY;
windowRect.right -= Context->SystemEdgeX - borderX;
windowRect.bottom -= Context->SystemEdgeY - borderY;
FillRect(hdc, &windowRect, GetSysColorBrush(COLOR_WINDOW));
}
}
VOID PhTnpInitializeTooltips(
_In_ PPH_TREENEW_CONTEXT Context
)
{
TOOLINFO toolInfo;
Context->TooltipsHandle = CreateWindowEx(
WS_EX_TRANSPARENT, // solves double-click problem
TOOLTIPS_CLASS,
NULL,
WS_POPUP | TTS_NOPREFIX,
0,
0,
0,
0,
NULL,
NULL,
Context->InstanceHandle,
NULL
);
if (!Context->TooltipsHandle)
return;
// Item tooltips
memset(&toolInfo, 0, sizeof(TOOLINFO));
toolInfo.cbSize = sizeof(TOOLINFO);
toolInfo.uFlags = TTF_TRANSPARENT;
toolInfo.hwnd = Context->Handle;
toolInfo.uId = TNP_TOOLTIPS_ITEM;
toolInfo.lpszText = LPSTR_TEXTCALLBACK;
toolInfo.lParam = TNP_TOOLTIPS_ITEM;
SendMessage(Context->TooltipsHandle, TTM_ADDTOOL, 0, (LPARAM)&toolInfo);
// Fixed column tooltips
toolInfo.uFlags = 0;
toolInfo.hwnd = Context->FixedHeaderHandle;
toolInfo.uId = TNP_TOOLTIPS_FIXED_HEADER;
toolInfo.lpszText = LPSTR_TEXTCALLBACK;
toolInfo.lParam = TNP_TOOLTIPS_FIXED_HEADER;
SendMessage(Context->TooltipsHandle, TTM_ADDTOOL, 0, (LPARAM)&toolInfo);
// Normal column tooltips
toolInfo.uFlags = 0;
toolInfo.hwnd = Context->HeaderHandle;
toolInfo.uId = TNP_TOOLTIPS_HEADER;
toolInfo.lpszText = LPSTR_TEXTCALLBACK;
toolInfo.lParam = TNP_TOOLTIPS_HEADER;
SendMessage(Context->TooltipsHandle, TTM_ADDTOOL, 0, (LPARAM)&toolInfo);
// Hook the header control window procedures so we can forward mouse messages to the tooltip
// control.
Context->FixedHeaderOldWndProc = (WNDPROC)GetWindowLongPtr(Context->FixedHeaderHandle, GWLP_WNDPROC);
SetProp(Context->FixedHeaderHandle, PhTnpMakeContextAtom(), (HANDLE)Context);
SetWindowLongPtr(Context->FixedHeaderHandle, GWLP_WNDPROC, (LONG_PTR)PhTnpHeaderHookWndProc);
Context->HeaderOldWndProc = (WNDPROC)GetWindowLongPtr(Context->HeaderHandle, GWLP_WNDPROC);
SetProp(Context->HeaderHandle, PhTnpMakeContextAtom(), (HANDLE)Context);
SetWindowLongPtr(Context->HeaderHandle, GWLP_WNDPROC, (LONG_PTR)PhTnpHeaderHookWndProc);
SendMessage(Context->TooltipsHandle, TTM_SETMAXTIPWIDTH, 0, MAXSHORT); // no limit
SendMessage(Context->TooltipsHandle, WM_SETFONT, (WPARAM)Context->Font, FALSE);
Context->TooltipFont = Context->Font;
}
VOID PhTnpGetTooltipText(
_In_ PPH_TREENEW_CONTEXT Context,
_In_ PPOINT Point,
_Out_ PWSTR *Text
)
{
PH_TREENEW_HIT_TEST hitTest;
BOOLEAN unfoldingTooltip;
BOOLEAN unfoldingTooltipFromViewCancelled;
PH_TREENEW_CELL_PARTS parts;
LONG viewRight;
PH_TREENEW_GET_CELL_TOOLTIP getCellTooltip;
hitTest.Point = *Point;
hitTest.InFlags = TN_TEST_COLUMN | TN_TEST_SUBITEM;
PhTnpHitTest(Context, &hitTest);
if (Context->DragSelectionActive)
return;
if (!(hitTest.Flags & TN_HIT_ITEM))
return;
if (hitTest.Flags & (TN_HIT_ITEM_PLUSMINUS | TN_HIT_DIVIDER))
return;
if (!hitTest.Column)
return;
if (Context->TooltipIndex != hitTest.Node->Index || Context->TooltipId != hitTest.Column->Id)
{
Context->TooltipIndex = hitTest.Node->Index;
Context->TooltipId = hitTest.Column->Id;
getCellTooltip.Flags = 0;
getCellTooltip.Node = hitTest.Node;
getCellTooltip.Column = hitTest.Column;
getCellTooltip.Unfolding = FALSE;
PhInitializeEmptyStringRef(&getCellTooltip.Text);
getCellTooltip.Font = Context->Font;
getCellTooltip.MaximumWidth = -1;
unfoldingTooltip = FALSE;
unfoldingTooltipFromViewCancelled = FALSE;
if (!(Context->ExtendedFlags & TN_FLAG_NO_UNFOLDING_TOOLTIPS) &&
PhTnpGetCellParts(Context, hitTest.Node->Index, hitTest.Column, TN_MEASURE_TEXT, &parts) &&
(parts.Flags & TN_PART_CONTENT) && (parts.Flags & TN_PART_TEXT))
{
viewRight = Context->ClientRect.right - (Context->VScrollVisible ? Context->VScrollWidth : 0);
// Use an unfolding tooltip if the text was truncated within the column, or the text
// extends beyond the view area in either direction.
if (parts.TextRect.left < parts.ContentRect.left || parts.TextRect.right > parts.ContentRect.right)
{
unfoldingTooltip = TRUE;
}
else if ((!hitTest.Column->Fixed && parts.TextRect.left < Context->NormalLeft) || parts.TextRect.right > viewRight)
{
// Only show view-based unfolding tooltips if the mouse is over the text itself.
if (Point->x >= parts.TextRect.left && Point->x < parts.TextRect.right)
unfoldingTooltip = TRUE;
else
unfoldingTooltipFromViewCancelled = TRUE;
}
if (unfoldingTooltip)
{
getCellTooltip.Unfolding = TRUE;
getCellTooltip.Text = parts.Text;
getCellTooltip.Font = parts.Font; // try to use the same font as the cell
Context->TooltipRect = parts.TextRect;
}
}
Context->Callback(Context->Handle, TreeNewGetCellTooltip, &getCellTooltip, NULL, Context->CallbackContext);
Context->TooltipUnfolding = getCellTooltip.Unfolding;
if (getCellTooltip.Text.Buffer && getCellTooltip.Text.Length != 0)
{
PhMoveReference(&Context->TooltipText, PhCreateString2(&getCellTooltip.Text));
}
else
{
PhClearReference(&Context->TooltipText);
if (unfoldingTooltipFromViewCancelled)
{
// We may need to show the view-based unfolding tooltip if the mouse moves over the
// text in the future. Reset the index and ID to make sure we keep checking.
Context->TooltipIndex = -1;
Context->TooltipId = -1;
}
}
Context->NewTooltipFont = getCellTooltip.Font;
if (!Context->NewTooltipFont)
Context->NewTooltipFont = Context->Font;
if (getCellTooltip.MaximumWidth <= MAXSHORT) // seems to be the maximum value that the tooltip control supports
SendMessage(Context->TooltipsHandle, TTM_SETMAXTIPWIDTH, 0, getCellTooltip.MaximumWidth);
else
SendMessage(Context->TooltipsHandle, TTM_SETMAXTIPWIDTH, 0, MAXSHORT);
}
if (Context->TooltipText)
*Text = Context->TooltipText->Buffer;
}
BOOLEAN PhTnpPrepareTooltipShow(
_In_ PPH_TREENEW_CONTEXT Context
)
{
RECT rect;
if (Context->TooltipFont != Context->NewTooltipFont)
{
Context->TooltipFont = Context->NewTooltipFont;
SendMessage(Context->TooltipsHandle, WM_SETFONT, (WPARAM)Context->TooltipFont, FALSE);
}
if (!Context->TooltipUnfolding)
{
SetWindowPos(
Context->TooltipsHandle,
HWND_TOPMOST,
0,
0,
0,
0,
SWP_NOSIZE | SWP_NOMOVE | SWP_NOACTIVATE | SWP_HIDEWINDOW
);
return FALSE;
}
rect = Context->TooltipRect;
SendMessage(Context->TooltipsHandle, TTM_ADJUSTRECT, TRUE, (LPARAM)&rect);
MapWindowPoints(Context->Handle, NULL, (POINT *)&rect, 2);
SetWindowPos(
Context->TooltipsHandle,
HWND_TOPMOST,
rect.left,
rect.top,
0,
0,
SWP_NOSIZE | SWP_NOACTIVATE | SWP_HIDEWINDOW
);
return TRUE;
}
VOID PhTnpPrepareTooltipPop(
_In_ PPH_TREENEW_CONTEXT Context
)
{
Context->TooltipIndex = -1;
Context->TooltipId = -1;
Context->TooltipColumnId = -1;
}
VOID PhTnpPopTooltip(
_In_ PPH_TREENEW_CONTEXT Context
)
{
if (Context->TooltipsHandle)
{
SendMessage(Context->TooltipsHandle, TTM_POP, 0, 0);
PhTnpPrepareTooltipPop(Context);
}
}
PPH_TREENEW_COLUMN PhTnpHitTestHeader(
_In_ PPH_TREENEW_CONTEXT Context,
_In_ BOOLEAN Fixed,
_In_ PPOINT Point,
_Out_opt_ PRECT ItemRect
)
{
PPH_TREENEW_COLUMN column;
RECT itemRect;
if (Fixed)
{
if (!Context->FixedColumnVisible)
return NULL;
column = Context->FixedColumn;
if (!Header_GetItemRect(Context->FixedHeaderHandle, 0, &itemRect))
return NULL;
}
else
{
HDHITTESTINFO hitTestInfo;
hitTestInfo.pt = *Point;
hitTestInfo.flags = 0;
hitTestInfo.iItem = -1;
if (SendMessage(Context->HeaderHandle, HDM_HITTEST, 0, (LPARAM)&hitTestInfo) != -1 && hitTestInfo.iItem != -1)
{
HDITEM item;
item.mask = HDI_LPARAM;
if (!Header_GetItem(Context->HeaderHandle, hitTestInfo.iItem, &item))
return NULL;
column = (PPH_TREENEW_COLUMN)item.lParam;
if (!Header_GetItemRect(Context->HeaderHandle, hitTestInfo.iItem, &itemRect))
return NULL;
}
else
{
return NULL;
}
}
if (ItemRect)
*ItemRect = itemRect;
return column;
}
VOID PhTnpGetHeaderTooltipText(
_In_ PPH_TREENEW_CONTEXT Context,
_In_ BOOLEAN Fixed,
_In_ PPOINT Point,
_Out_ PWSTR *Text
)
{
LOGICAL result;
PPH_TREENEW_COLUMN column;
RECT itemRect;
PWSTR text;
SIZE_T textCount;
HDC hdc;
SIZE textSize;
column = PhTnpHitTestHeader(Context, Fixed, Point, &itemRect);
if (!column)
return;
if (Context->TooltipColumnId != column->Id)
{
// Determine if the tooltip needs to be shown.
text = column->Text;
textCount = PhCountStringZ(text);
if (!(hdc = GetDC(Context->Handle)))
return;
SelectObject(hdc, Context->Font);
result = GetTextExtentPoint32(hdc, text, (ULONG)textCount, &textSize);
ReleaseDC(Context->Handle, hdc);
if (!result)
return;
if (textSize.cx + 6 + 6 <= itemRect.right - itemRect.left) // HACK: Magic values (same as our cell margins?)
return;
Context->TooltipColumnId = column->Id;
PhMoveReference(&Context->TooltipText, PhCreateStringEx(text, textCount * sizeof(WCHAR)));
}
*Text = Context->TooltipText->Buffer;
// Always use the default parameters for column header tooltips.
Context->NewTooltipFont = Context->Font;
Context->TooltipUnfolding = FALSE;
SendMessage(Context->TooltipsHandle, TTM_SETMAXTIPWIDTH, 0, TNP_TOOLTIPS_DEFAULT_MAXIMUM_WIDTH);
}
PWSTR PhTnpMakeContextAtom(
VOID
)
{
PH_DEFINE_MAKE_ATOM(L"PhLib_TreeNewContext");
}
LRESULT CALLBACK PhTnpHeaderHookWndProc(
_In_ HWND hwnd,
_In_ UINT uMsg,
_In_ WPARAM wParam,
_In_ LPARAM lParam
)
{
PPH_TREENEW_CONTEXT context;
WNDPROC oldWndProc;
context = (PPH_TREENEW_CONTEXT)GetProp(hwnd, PhTnpMakeContextAtom());
if (hwnd == context->FixedHeaderHandle)
oldWndProc = context->FixedHeaderOldWndProc;
else
oldWndProc = context->HeaderOldWndProc;
switch (uMsg)
{
case WM_DESTROY:
{
SetWindowLongPtr(hwnd, GWLP_WNDPROC, (LONG_PTR)oldWndProc);
RemoveProp(hwnd, PhTnpMakeContextAtom());
}
break;
case WM_MOUSEMOVE:
{
POINT point;
PPH_TREENEW_COLUMN column;
ULONG id;
point.x = GET_X_LPARAM(lParam);
point.y = GET_Y_LPARAM(lParam);
column = PhTnpHitTestHeader(context, hwnd == context->FixedHeaderHandle, &point, NULL);
if (column)
id = column->Id;
else
id = -1;
if (context->TooltipColumnId != id)
{
PhTnpPopTooltip(context);
}
}
break;
case WM_NOTIFY:
{
NMHDR *header = (NMHDR *)lParam;
switch (header->code)
{
case TTN_GETDISPINFO:
{
if (header->hwndFrom == context->TooltipsHandle)
{
NMTTDISPINFO *info = (NMTTDISPINFO *)header;
POINT point;
PhTnpGetMessagePos(hwnd, &point);
PhTnpGetHeaderTooltipText(context, info->lParam == TNP_TOOLTIPS_FIXED_HEADER, &point, &info->lpszText);
}
}
break;
case TTN_SHOW:
{
if (header->hwndFrom == context->TooltipsHandle)
{
return PhTnpPrepareTooltipShow(context);
}
}
break;
case TTN_POP:
{
if (header->hwndFrom == context->TooltipsHandle)
{
PhTnpPrepareTooltipPop(context);
}
}
break;
}
}
break;
}
switch (uMsg)
{
case WM_MOUSEMOVE:
case WM_LBUTTONDOWN:
case WM_LBUTTONUP:
case WM_RBUTTONDOWN:
case WM_RBUTTONUP:
case WM_MBUTTONDOWN:
case WM_MBUTTONUP:
{
if (context->TooltipsHandle)
{
MSG message;
message.hwnd = hwnd;
message.message = uMsg;
message.wParam = wParam;
message.lParam = lParam;
SendMessage(context->TooltipsHandle, TTM_RELAYEVENT, 0, (LPARAM)&message);
}
}
break;
}
return CallWindowProc(oldWndProc, hwnd, uMsg, wParam, lParam);
}
BOOLEAN PhTnpDetectDrag(
_In_ PPH_TREENEW_CONTEXT Context,
_In_ LONG CursorX,
_In_ LONG CursorY,
_In_ BOOLEAN DispatchMessages,
_Out_opt_ PULONG CancelledByMessage
)
{
RECT dragRect;
MSG msg;
// Capture mouse input and see if the user moves the mouse beyond the drag rectangle.
dragRect.left = CursorX - Context->SystemDragX;
dragRect.top = CursorY - Context->SystemDragY;
dragRect.right = CursorX + Context->SystemDragX;
dragRect.bottom = CursorY + Context->SystemDragY;
MapWindowPoints(Context->Handle, NULL, (POINT *)&dragRect, 2);
SetCapture(Context->Handle);
if (CancelledByMessage)
*CancelledByMessage = 0;
do
{
// It seems that GetMessage dispatches nonqueued messages directly from kernel-mode, so we
// have to use PeekMessage and WaitMessage in order to process WM_CAPTURECHANGED messages.
if (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE))
{
switch (msg.message)
{
case WM_LBUTTONDOWN:
case WM_LBUTTONUP:
case WM_RBUTTONDOWN:
case WM_RBUTTONUP:
ReleaseCapture();
if (CancelledByMessage)
*CancelledByMessage = msg.message;
break;
case WM_MOUSEMOVE:
if (msg.pt.x < dragRect.left || msg.pt.x >= dragRect.right ||
msg.pt.y < dragRect.top || msg.pt.y >= dragRect.bottom)
{
if (IsWindow(Context->Handle))
return TRUE;
else
return FALSE;
}
break;
default:
if (DispatchMessages)
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
break;
}
}
else
{
WaitMessage();
}
} while (IsWindow(Context->Handle) && GetCapture() == Context->Handle);
return FALSE;
}
VOID PhTnpDragSelect(
_In_ PPH_TREENEW_CONTEXT Context,
_In_ LONG CursorX,
_In_ LONG CursorY
)
{
MSG msg;
LONG cursorX;
LONG cursorY;
BOOLEAN originFixed;
RECT dragRect;
RECT oldDragRect;
RECT windowRect;
POINT cursorPoint;
BOOLEAN showContextMenu;
cursorX = CursorX;
cursorY = CursorY;
originFixed = cursorX < Context->FixedWidth;
dragRect.left = cursorX;
dragRect.top = cursorY;
dragRect.right = cursorX;
dragRect.bottom = cursorY;
oldDragRect = dragRect;
Context->DragRect = dragRect;
Context->DragSelectionActive = TRUE;
if (Context->DoubleBuffered)
Context->SelectionRectangleAlpha = TRUE;
// TODO: Make sure the monitor's color depth is sufficient for alpha-blended selection
// rectangles.
GetWindowRect(Context->Handle, &windowRect);
cursorPoint.x = windowRect.left + cursorX;
cursorPoint.y = windowRect.top + cursorY;
showContextMenu = FALSE;
SetCapture(Context->Handle);
while (TRUE)
{
if (!PeekMessage(&msg, NULL, 0, 0, PM_REMOVE))
{
BOOLEAN leftOrRight;
BOOLEAN aboveOrBelow;
// If the cursor is outside of the window, generate some messages so the window keeps
// scrolling.
leftOrRight = cursorPoint.x < windowRect.left || cursorPoint.x > windowRect.right;
aboveOrBelow = cursorPoint.y < windowRect.top || cursorPoint.y > windowRect.bottom;
if ((Context->VScrollVisible && aboveOrBelow && PhTnpCanScroll(Context, FALSE, cursorPoint.y > windowRect.bottom)) ||
(Context->HScrollVisible && leftOrRight && PhTnpCanScroll(Context, TRUE, cursorPoint.x > windowRect.right)))
{
SetCursorPos(cursorPoint.x, cursorPoint.y);
}
else
{
WaitMessage();
}
goto EndOfLoop;
}
cursorPoint = msg.pt;
switch (msg.message)
{
case WM_LBUTTONDOWN:
case WM_LBUTTONUP:
case WM_RBUTTONDOWN:
case WM_MBUTTONDOWN:
case WM_MBUTTONUP:
ReleaseCapture();
goto EndOfLoop;
case WM_RBUTTONUP:
ReleaseCapture();
showContextMenu = TRUE;
goto EndOfLoop;
case WM_MOUSEMOVE:
{
LONG newCursorX;
LONG newCursorY;
LONG deltaRows;
LONG deltaX;
LONG oldVScrollPosition;
LONG oldHScrollPosition;
LONG newDeltaX;
LONG newDeltaY;
LONG viewLeft;
LONG viewTop;
LONG viewRight;
LONG viewBottom;
LONG temp;
RECT totalRect;
newCursorX = GET_X_LPARAM(msg.lParam);
newCursorY = GET_Y_LPARAM(msg.lParam);
// Scroll the window if the cursor is outside of it.
deltaRows = 0;
deltaX = 0;
if (Context->VScrollVisible)
{
if (cursorPoint.y < windowRect.top)
deltaRows = -(windowRect.top - cursorPoint.y + Context->RowHeight - 1) / Context->RowHeight; // scroll up
else if (cursorPoint.y >= windowRect.bottom)
deltaRows = (cursorPoint.y - windowRect.bottom + Context->RowHeight - 1) / Context->RowHeight; // scroll down
}
if (Context->HScrollVisible)
{
if (cursorPoint.x < windowRect.left)
deltaX = -(windowRect.left - cursorPoint.x); // scroll left
else if (cursorPoint.x >= windowRect.right)
deltaX = cursorPoint.x - windowRect.right; // scroll right
}
oldVScrollPosition = Context->VScrollPosition;
oldHScrollPosition = Context->HScrollPosition;
if (deltaRows != 0 || deltaX != 0)
PhTnpScroll(Context, deltaRows, deltaX);
newDeltaX = oldHScrollPosition - Context->HScrollPosition;
newDeltaY = (oldVScrollPosition - Context->VScrollPosition) * Context->RowHeight;
// Adjust our original drag point for the scrolling.
if (!originFixed)
cursorX += newDeltaX;
cursorY += newDeltaY;
// Adjust the old drag rectangle for the scrolling.
if (!originFixed)
oldDragRect.left += newDeltaX;
oldDragRect.top += newDeltaY;
if (!originFixed)
oldDragRect.right += newDeltaX;
oldDragRect.bottom += newDeltaY;
// Ensure that the new cursor position is within the content area.
viewLeft = Context->FixedColumnVisible ? 0 : -Context->HScrollPosition;
viewTop = Context->HeaderHeight - Context->VScrollPosition;
viewRight = Context->NormalLeft + Context->TotalViewX - Context->HScrollPosition;
viewBottom = Context->HeaderHeight + ((LONG)Context->FlatList->Count - Context->VScrollPosition) * Context->RowHeight;
temp = Context->ClientRect.right - (Context->VScrollVisible ? Context->VScrollWidth : 0);
viewRight = max(viewRight, temp);
temp = Context->ClientRect.bottom - ((!Context->FixedColumnVisible && Context->HScrollVisible) ? Context->HScrollHeight : 0);
viewBottom = max(viewBottom, temp);
if (newCursorX < viewLeft)
newCursorX = viewLeft;
if (newCursorX > viewRight)
newCursorX = viewRight;
if (newCursorY < viewTop)
newCursorY = viewTop;
if (newCursorY > viewBottom)
newCursorY = viewBottom;
// Create the new drag rectangle.
if (cursorX < newCursorX)
{
dragRect.left = cursorX;
dragRect.right = newCursorX;
}
else
{
dragRect.left = newCursorX;
dragRect.right = cursorX;
}
if (cursorY < newCursorY)
{
dragRect.top = cursorY;
dragRect.bottom = newCursorY;
}
else
{
dragRect.top = newCursorY;
dragRect.bottom = cursorY;
}
// Has anything changed from before?
if (dragRect.left == oldDragRect.left && dragRect.top == oldDragRect.top &&
dragRect.right == oldDragRect.right && dragRect.bottom == oldDragRect.bottom)
{
break;
}
Context->DragRect = dragRect;
// Process the selection.
totalRect.left = min(dragRect.left, oldDragRect.left);
totalRect.top = min(dragRect.top, oldDragRect.top);
totalRect.right = max(dragRect.right, oldDragRect.right);
totalRect.bottom = max(dragRect.bottom, oldDragRect.bottom);
PhTnpProcessDragSelect(Context, (ULONG)msg.wParam, &oldDragRect, &dragRect, &totalRect);
// Redraw the drag rectangle.
RedrawWindow(Context->Handle, &totalRect, NULL, RDW_INVALIDATE | RDW_UPDATENOW);
oldDragRect = dragRect;
}
break;
case WM_MOUSELEAVE:
break; // don't process
case WM_MOUSEWHEEL:
break; // don't process
case WM_KEYDOWN:
if (msg.wParam == VK_ESCAPE)
{
ULONG changedStart;
ULONG changedEnd;
RECT rect;
PhTnpSelectRange(Context, -1, -1, TN_SELECT_RESET, &changedStart, &changedEnd);
if (PhTnpGetRowRects(Context, changedStart, changedEnd, TRUE, &rect))
{
InvalidateRect(Context->Handle, &rect, FALSE);
}
ReleaseCapture();
}
break; // don't process
case WM_CHAR:
break; // don't process
default:
TranslateMessage(&msg);
DispatchMessage(&msg);
break;
}
EndOfLoop:
if (GetCapture() != Context->Handle)
break;
}
Context->DragSelectionActive = FALSE;
RedrawWindow(Context->Handle, &dragRect, NULL, RDW_INVALIDATE | RDW_UPDATENOW);
if (showContextMenu)
{
// Display a context menu at the original drag point.
SendMessage(Context->Handle, WM_CONTEXTMENU, (WPARAM)Context->Handle, MAKELPARAM(windowRect.left + CursorX, windowRect.top + CursorY));
}
}
VOID PhTnpProcessDragSelect(
_In_ PPH_TREENEW_CONTEXT Context,
_In_ ULONG VirtualKeys,
_In_ PRECT OldRect,
_In_ PRECT NewRect,
_In_ PRECT TotalRect
)
{
LONG firstRow;
LONG lastRow;
RECT rowRect;
LONG i;
PPH_TREENEW_NODE node;
LONG changedStart;
LONG changedEnd;
RECT rect;
// Determine which rows we need to test. The divisions below must be done on positive integers
// to ensure correct rounding.
firstRow = (TotalRect->top - Context->HeaderHeight + Context->VScrollPosition * Context->RowHeight) / Context->RowHeight;
lastRow = (TotalRect->bottom - 1 - Context->HeaderHeight + Context->VScrollPosition * Context->RowHeight) / Context->RowHeight;
if (firstRow < 0)
firstRow = 0;
if (lastRow >= (LONG)Context->FlatList->Count)
lastRow = Context->FlatList->Count - 1;
rowRect.left = 0;
rowRect.top = Context->HeaderHeight + (firstRow - Context->VScrollPosition) * Context->RowHeight;
rowRect.right = Context->NormalLeft + Context->TotalViewX - Context->HScrollPosition;
rowRect.bottom = rowRect.top + Context->RowHeight;
changedStart = lastRow;
changedEnd = firstRow;
// Process the rows.
for (i = firstRow; i <= lastRow; i++)
{
BOOLEAN inOldRect;
BOOLEAN inNewRect;
node = Context->FlatList->Items[i];
inOldRect = rowRect.top < OldRect->bottom && rowRect.bottom > OldRect->top &&
rowRect.left < OldRect->right && rowRect.right > OldRect->left;
inNewRect = rowRect.top < NewRect->bottom && rowRect.bottom > NewRect->top &&
rowRect.left < NewRect->right && rowRect.right > NewRect->left;
if (VirtualKeys & MK_CONTROL)
{
if (!node->Unselectable && inOldRect != inNewRect)
{
node->Selected = !node->Selected;
if (changedStart > i)
changedStart = i;
if (changedEnd < i)
changedEnd = i;
}
}
else
{
if (!node->Unselectable && inOldRect != inNewRect)
{
node->Selected = inNewRect;
if (changedStart > i)
changedStart = i;
if (changedEnd < i)
changedEnd = i;
}
}
rowRect.top = rowRect.bottom;
rowRect.bottom += Context->RowHeight;
}
if (changedStart <= changedEnd)
{
Context->Callback(Context->Handle, TreeNewSelectionChanged, NULL, NULL, Context->CallbackContext);
}
if (PhTnpGetRowRects(Context, changedStart, changedEnd, TRUE, &rect))
{
InvalidateRect(Context->Handle, &rect, FALSE);
}
}
VOID PhTnpCreateBufferedContext(
_In_ PPH_TREENEW_CONTEXT Context
)
{
HDC hdc;
if (hdc = GetDC(Context->Handle))
{
Context->BufferedContext = CreateCompatibleDC(hdc);
if (!Context->BufferedContext)
return;
Context->BufferedContextRect = Context->ClientRect;
Context->BufferedBitmap = CreateCompatibleBitmap(
hdc,
Context->BufferedContextRect.right + 1, // leave one extra pixel for divider animation
Context->BufferedContextRect.bottom
);
if (!Context->BufferedBitmap)
{
DeleteDC(Context->BufferedContext);
Context->BufferedContext = NULL;
return;
}
ReleaseDC(Context->Handle, hdc);
Context->BufferedOldBitmap = SelectObject(Context->BufferedContext, Context->BufferedBitmap);
}
}
VOID PhTnpDestroyBufferedContext(
_In_ PPH_TREENEW_CONTEXT Context
)
{
// The original bitmap must be selected back into the context, otherwise the bitmap can't be
// deleted.
SelectObject(Context->BufferedContext, Context->BufferedOldBitmap);
DeleteObject(Context->BufferedBitmap);
DeleteDC(Context->BufferedContext);
Context->BufferedContext = NULL;
Context->BufferedBitmap = NULL;
}
VOID PhTnpGetMessagePos(
_In_ HWND hwnd,
_Out_ PPOINT ClientPoint
)
{
ULONG position;
POINT point;
position = GetMessagePos();
point.x = GET_X_LPARAM(position);
point.y = GET_Y_LPARAM(position);
ScreenToClient(hwnd, &point);
*ClientPoint = point;
}