diff --git a/2.x/trunk/ProcessHacker/proctree.c b/2.x/trunk/ProcessHacker/proctree.c index 145956fd5..5f98579bb 100644 --- a/2.x/trunk/ProcessHacker/proctree.c +++ b/2.x/trunk/ProcessHacker/proctree.c @@ -88,6 +88,7 @@ VOID PhInitializeProcessTreeList( ProcessTreeListHandle = hwnd; PhSetControlTheme(ProcessTreeListHandle, L"explorer"); + TreeNew_SetExtendedFlags(hwnd, TN_FLAG_ITEM_DRAG_SELECT, TN_FLAG_ITEM_DRAG_SELECT); SendMessage(TreeNew_GetTooltips(ProcessTreeListHandle), TTM_SETMAXTIPWIDTH, 0, MAXSHORT); SendMessage(TreeNew_GetTooltips(ProcessTreeListHandle), TTM_SETDELAYTIME, TTDT_AUTOPOP, 0x7fff); diff --git a/2.x/trunk/phlib/include/treenew.h b/2.x/trunk/phlib/include/treenew.h index a18fc03ba..9be17c4d4 100644 --- a/2.x/trunk/phlib/include/treenew.h +++ b/2.x/trunk/phlib/include/treenew.h @@ -91,6 +91,9 @@ typedef struct _PH_TREENEW_NODE #define TN_STYLE_NO_DIVIDER 0x4 #define TN_STYLE_ANIMATE_DIVIDER 0x8 +// Extended flags +#define TN_FLAG_ITEM_DRAG_SELECT 0x1 + // Callback flags #define TN_CACHE 0x1 #define TN_AUTO_FORECOLOR 0x1000 @@ -344,7 +347,8 @@ typedef struct _PH_TREENEW_SEARCH_EVENT #define TNM_SETFOCUSNODE (WM_USER + 35) #define TNM_SETMARKNODE (WM_USER + 36) #define TNM_SETHOTNODE (WM_USER + 37) -#define TNM_LAST (WM_USER + 37) +#define TNM_SETEXTENDEDFLAGS (WM_USER + 38) +#define TNM_LAST (WM_USER + 38) #define TreeNew_SetCallback(hWnd, Callback, Context) \ SendMessage((hWnd), TNM_SETCALLBACK, (WPARAM)(Context), (LPARAM)(Callback)) @@ -451,6 +455,9 @@ typedef struct _PH_TREENEW_SEARCH_EVENT #define TreeNew_SetHotNode(hWnd, Node) \ SendMessage((hWnd), TNM_SETHOTNODE, 0, (LPARAM)(Node)) +#define TreeNew_SetExtendedFlags(hWnd, Mask, Value) \ + SendMessage((hWnd), TNM_SETEXTENDEDFLAGS, (WPARAM)(Mask), (LPARAM)(Value)) + typedef struct _PH_TREENEW_VIEW_PARTS { RECT ClientRect; diff --git a/2.x/trunk/phlib/include/treenewp.h b/2.x/trunk/phlib/include/treenewp.h index 764f28e3d..8d0934dee 100644 --- a/2.x/trunk/phlib/include/treenewp.h +++ b/2.x/trunk/phlib/include/treenewp.h @@ -64,6 +64,7 @@ typedef struct _PH_TREENEW_CONTEXT }; ULONG Style; ULONG ExtendedStyle; + ULONG ExtendedFlags; HFONT Font; HCURSOR Cursor; @@ -343,6 +344,16 @@ VOID PhTnpSetRedraw( __in BOOLEAN Redraw ); +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 + ); + // Columns PPH_TREENEW_COLUMN PhTnpLookupColumnById( @@ -493,6 +504,14 @@ VOID PhTnpSetHotNode( __in BOOLEAN NewPlusMinusHot ); +VOID PhTnpProcessSelectNode( + __in PPH_TREENEW_CONTEXT Context, + __in PPH_TREENEW_NODE Node, + __in LOGICAL ControlKey, + __in LOGICAL ShiftKey, + __in LOGICAL RightButton + ); + BOOLEAN PhTnpEnsureVisibleNode( __in PPH_TREENEW_CONTEXT Context, __in ULONG Index @@ -643,7 +662,8 @@ BOOLEAN PhTnpDetectDrag( __in PPH_TREENEW_CONTEXT Context, __in LONG CursorX, __in LONG CursorY, - __in BOOLEAN DispatchMessages + __in BOOLEAN DispatchMessages, + __out_opt PULONG CancelledByMessage ); VOID PhTnpDragSelect( diff --git a/2.x/trunk/phlib/treenew.c b/2.x/trunk/phlib/treenew.c index e4f328fe1..735206ffb 100644 --- a/2.x/trunk/phlib/treenew.c +++ b/2.x/trunk/phlib/treenew.c @@ -794,100 +794,155 @@ VOID PhTnpOnXxxButtonXxx( if (!(hitTest.Flags & TN_HIT_ITEM_PLUSMINUS) && (Message == WM_LBUTTONDOWN || Message == WM_RBUTTONDOWN)) { + LOGICAL realHitItem; + PH_TREENEW_CELL_PARTS parts; + + PhTnpPopTooltip(Context); + if (hitTest.Flags & TN_HIT_ITEM) { Context->FocusNode = hitTest.Node; + } - if (Message == WM_RBUTTONDOWN) + realHitItem = hitTest.Flags & TN_HIT_ITEM; + + if (realHitItem && (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. If it isn't, then + // don't count that as a hit. The exception is when the item is already + // selected - we always count that as a hit. Also, we always count it as + // a hit if user is beginning to drag the divider. + + if (!hitTest.Node->Selected && !startingTracking) { - // 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 && !hitTest.Node->Selected) + if (PhTnpGetCellParts(Context, hitTest.Node->Index, hitTest.Column, TN_MEASURE_TEXT, &parts)) { - PhTnpSelectRange(Context, hitTest.Node->Index, hitTest.Node->Index, TN_SELECT_RESET, &changedStart, &changedEnd); - Context->MarkNodeIndex = hitTest.Node->Index; + realHitItem = FALSE; - if (PhTnpGetRowRects(Context, changedStart, changedEnd, TRUE, &rect)) + if ((parts.Flags & TN_PART_ICON) && CursorX >= parts.IconRect.left && CursorX < parts.IconRect.right) + realHitItem = TRUE; + + if (parts.Flags & TN_PART_TEXT) { - InvalidateRect(hwnd, &rect, FALSE); + if (!(hitTest.Column->TextFlags & (DT_CENTER | DT_RIGHT))) + { + if (CursorX >= parts.TextRect.left && CursorX < parts.TextRect.right) + realHitItem = TRUE; + } + else if (parts.Flags & TN_PART_CONTENT) + { + // PhTnpGetCellParts doesn't take into account text alignment. If there's text and + // the cursor is in the content part, it's a hit. + if (parts.Text.Length != 0 && CursorX >= parts.ContentRect.left && CursorX < parts.ContentRect.right) + realHitItem = TRUE; + } } } } - 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 (hitTest.Node->Index > Context->MarkNodeIndex) - { - start = Context->MarkNodeIndex; - end = hitTest.Node->Index; - } - else - { - start = hitTest.Node->Index; - end = Context->MarkNodeIndex; - } - - PhTnpSelectRange(Context, start, end, TN_SELECT_RESET, &changedStart, &changedEnd); - - if (PhTnpGetRowRects(Context, changedStart, changedEnd, TRUE, &rect)) - { - InvalidateRect(hwnd, &rect, FALSE); - } - } - else if (controlKey) - { - // Control key: toggle the selection on the current node, and also make it the selection mark. - - PhTnpSelectRange(Context, hitTest.Node->Index, hitTest.Node->Index, TN_SELECT_TOGGLE, NULL, NULL); - Context->MarkNodeIndex = hitTest.Node->Index; - - if (PhTnpGetRowRects(Context, hitTest.Node->Index, hitTest.Node->Index, TRUE, &rect)) - { - InvalidateRect(hwnd, &rect, FALSE); - } - } - else - { - // Normal: select the current node, and also make it the selection mark. - - PhTnpSelectRange(Context, hitTest.Node->Index, hitTest.Node->Index, TN_SELECT_RESET, &changedStart, &changedEnd); - Context->MarkNodeIndex = hitTest.Node->Index; - - if (PhTnpGetRowRects(Context, changedStart, changedEnd, TRUE, &rect)) - { - InvalidateRect(hwnd, &rect, FALSE); - } - } - - PhTnpPopTooltip(Context); + if (realHitItem) + { + PhTnpProcessSelectNode(Context, hitTest.Node, controlKey, shiftKey, Message == WM_RBUTTONDOWN); } else { BOOLEAN dragSelect; + ULONG indexToSelect; + BOOLEAN selectionProcessed; dragSelect = FALSE; + indexToSelect = -1; + selectionProcessed = FALSE; if (!(hitTest.Flags & (TN_HIT_LEFT | TN_HIT_RIGHT | TN_HIT_ABOVE | TN_HIT_BELOW))) { - // Check for drag selection. - if (PhTnpDetectDrag(Context, CursorX, CursorY, TRUE)) + 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, but we didn't count this as a hit earlier. + // It's a hit now. + PhTnpProcessSelectNode(Context, hitTest.Node, controlKey, shiftKey, Message == WM_RBUTTONDOWN); + 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 (!controlKey && !shiftKey) + if (!selectionProcessed && !controlKey && !shiftKey) { // Nothing: deselect everything. - PhTnpSelectRange(Context, -1, -1, TN_SELECT_RESET, &changedStart, &changedEnd); + PhTnpSelectRange(Context, indexToSelect, indexToSelect, TN_SELECT_RESET, &changedStart, &changedEnd); if (PhTnpGetRowRects(Context, changedStart, changedEnd, TRUE, &rect)) { @@ -899,10 +954,15 @@ VOID PhTnpOnXxxButtonXxx( { PhTnpDragSelect(Context, CursorX, CursorY); } + + 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; @@ -947,17 +1007,8 @@ VOID PhTnpOnXxxButtonXxx( if (!(hitTest.Flags & TN_HIT_ITEM_PLUSMINUS) && clickMessage != -1) { - PH_TREENEW_MOUSE_EVENT mouseEvent; - - mouseEvent.Location.x = CursorX; - mouseEvent.Location.y = CursorY; - mouseEvent.Node = hitTest.Node; - mouseEvent.Column = hitTest.Column; - mouseEvent.KeyFlags = VirtualKeys; - Context->Callback(Context->Handle, clickMessage, &mouseEvent, NULL, Context->CallbackContext); + PhTnpSendMouseEvent(Context, clickMessage, CursorX, CursorY, hitTest.Node, hitTest.Column, VirtualKeys); } - - // TODO: Drag selection } VOID PhTnpOnCaptureChanged( @@ -1698,6 +1749,10 @@ ULONG_PTR PhTnpOnUserMessage( case TNM_SETHOTNODE: PhTnpSetHotNode(Context, (PPH_TREENEW_NODE)LParam, FALSE); return TRUE; + case TNM_SETEXTENDEDFLAGS: + Context->ExtendedFlags &= ~(ULONG)WParam; + Context->ExtendedFlags |= (ULONG)LParam; + return TRUE; } return 0; @@ -2003,6 +2058,26 @@ VOID PhTnpSetRedraw( } } +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 @@ -3347,6 +3422,87 @@ VOID PhTnpSetHotNode( } } +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 @@ -5316,7 +5472,8 @@ BOOLEAN PhTnpDetectDrag( __in PPH_TREENEW_CONTEXT Context, __in LONG CursorX, __in LONG CursorY, - __in BOOLEAN DispatchMessages + __in BOOLEAN DispatchMessages, + __out_opt PULONG CancelledByMessage ) { RECT dragRect; @@ -5332,6 +5489,9 @@ BOOLEAN PhTnpDetectDrag( SetCapture(Context->Handle); + if (CancelledByMessage) + *CancelledByMessage = 0; + do { // It seems that GetMessage dispatches nonqueued messages directly from kernel-mode, so @@ -5348,7 +5508,11 @@ BOOLEAN PhTnpDetectDrag( case WM_RBUTTONDOWN: case WM_RBUTTONUP: ReleaseCapture(); - return FALSE; + + 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) @@ -5575,7 +5739,7 @@ VOID PhTnpDragSelect( ULONG changedEnd; RECT rect; - PhTnpSelectRange(Context, -1, 0, TN_SELECT_RESET, &changedStart, &changedEnd); + PhTnpSelectRange(Context, -1, -1, TN_SELECT_RESET, &changedStart, &changedEnd); if (PhTnpGetRowRects(Context, changedStart, changedEnd, TRUE, &rect)) {