6663 lines
188 KiB
C
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, ÷rRect, 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, ÷rRect, 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 = ▭
|
|
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;
|
|
}
|