Tenant Pick Regions and Drop Sourcing

In Patron, I've defined the outer rectangular boundary of any tenant as the pick region, excluding those areas already occupied by sizing handles. The width and height of the region are the same as the dimensions of sizing handles.

In Chapter 12, we added the CPage::OnNCHitTest (PAGEMOUS.CPP) function to Patron to determine when the mouse was over a sizing handle. We used the return value from this function in CPage::SetCursor to show an appropriate mouse cursor for each sizing handle. For drag and drop, we modify OnNCHitTest to also check for the pick region (after the sizing) and modify CPage::SetCursor to show a four-pointed move cursor:

When OnNCHitTest determines that the mouse is within a pick region, it stores the code HTCAPTION in CPage::m_uHTCode, which CPage::SetCursor uses to set the move cursor. When CPage::OnLeftDown detects the HTCAPTION code, it calls CPage::DragDrop to create the data object (CPage::TransferObjectCreate) and the drop source object and then calls DoDragDrop. When the latter function returns, Patron checks whether a move effect occurred and handles the selected data appropriately.

Da Bears, Da Bulls, Da Bounce

Often end users accidentally press a mouse button so that it stays down for only a short period of time. This is called a mouse bounce. It is preferable for an application to avoid executing actions on accidental mouse clicks by debouncing the mouse.2 In Patron, we want to avoid the extra work to set up a drag-and-drop operation unless the end user really does want to do one. To do this, we need to debounce the mouse in the following way:

When the mouse button is pressed in the pick region, start a delay timer with a period defined by the DragDelay value in the [windows] section of WIN.INI. OLE's default DD_DEFDRAGDELAY value is 200 ms.

If the mouse moves a small distance away from its button down point before the timer expires, start the drag-and-drop operation. (Enough mouse movement means that the end user wants to drag the data.) The "small distance" is the number of pixels defined by the DragMinDist value in the [windows] section of WIN.INI. OLE's default DD_DEFDRAGMINDIST value is 2.

When the timer elapses, start the drag-and-drop operation.

The version of Patron in Chapter 12 contains code to do this for the sizing handles as well. Here's how it works. The CPage constructor (PAGE.CPP) reads the delay and distance values and stores them in the page object. It also initializes size pending and drag pending flags:


m_fDragPending=FALSE;
m_fSizePending=FALSE;

m_cxyDist=GetProfileInt(TEXT("windows"), TEXT("DragMinDist")
, DD_DEFDRAGMINDIST);
m_cDelay=GetProfileInt(TEXT("windows"), TEXT("DragDelay")
, DD_DEFDRAGDELAY);

When the left mouse button is pressed in a sizing handle or the pick region, CPage::OnLeftDown sets the type of operation pending, saves the exact location of the click, and starts a timer, as shown in the code for the drag operation. (The FALSE return value indicates that the mouse event doesn't modify the tenant.)


if (HTCAPTION==m_uHTCode)
{
m_fDragPending=TRUE;
m_ptDown.x=x;
m_ptDown.y=y;
m_uKeysDown=uKeys;
m_fTimer=TRUE;
SetTimer(m_hWnd, IDTIMER_DEBOUNCE, m_cDelay, NULL);
return FALSE;
}

When the timer expires, Patron's pages window (managed in CPages) picks up a WM_TIMER message and calls CPage::OnTimer (PAGEMOUS.CPP), which kills the timer and starts the type of operation that was pending:


void CPage::OnTimer(UINT uID)
{
if (m_fSizePending œœ m_fDragPending)
{
BOOL fSize=m_fSizePending;
BOOL fDrag=m_fDragPending;

m_fSizePending=FALSE;
m_fDragPending=FALSE;

KillTimer(m_hWnd, IDTIMER_DEBOUNCE);
m_fTimer=FALSE;

if (fDrag)
{
POINT pt;

GetCursorPos(&pt);
m_pPG->m_fDirty œ= DragDrop(m_uKeysDown
, m_ptDown.x, m_ptDown.y);
return;
}

if (fSize)
StartSizeTracking();
}

return;
}

If the mouse moves during this delay period, CPage::OnMouseMove is called. If there is a pending size or drag and the mouse has moved more than the minimum distance, the appropriate operation is started:


void CPage::OnMouseMove(UINT uKeys, int x, int y)
{
§

if (m_fSizePending œœ m_fDragPending)
{
int dx, dy;

dx=(x>m_ptDown.x) ? (x-m_ptDown.x) : (m_ptDown.x-x);
dy=(y>m_ptDown.y) ? (y-m_ptDown.y) : (m_ptDown.y-y);

if (dx>m_cxyDist œœ dy>m_cxyDist)
{
[Code like that in OnTimer to start a drag or a size]
}
[Other code omitted]
}

[Other code omitted]
}

The final thing to remember is to kill the timer if the mouse button is released before the timer elapses. This is done in CPage::OnLeftUp, which also resets both pending operation flags to FALSE.

Moving a Tenant on a Page

After a drag-and-drop operation is finished, Patron checks to see whether the source and the target were the same window, using the m_fDragSource flag as we did in Cosmo to avoid extra work. This flag is stored in the CPages class. Patron also has a flag in CPages named m_fMoveInPage, initially set to FALSE. Now look at IDropTarget::Drop in DROPTGT.CPP—if m_fDragSource is TRUE and the last effect was DROPEFFECT_MOVE, we can set m_fMoveInPage to TRUE. In all other cases, m_fMoveInPage remains FALSE. Setting m_fMoveInPage to TRUE tells CPage::DragDrop to do nothing more than move a tenant after DoDragDrop returns. This invalidates the tenant's old position (for repaint) and ensures that the new tenant position is clipped to the page boundaries. The following sequence of code performs all the operations necessary for this special case:


//In CPage::DragDrop
m_pPG->m_fDragSource=TRUE;
m_pPG->m_fMoveInPage=FALSE;
hr=DoDragDrop(...);

//In CDropTarget::Drop
if (m_pDoc->m_pPG->m_fDragSource && !(grfKeyState & MK_CONTROL))
{
*pdwEffect=DROPEFFECT_MOVE;
m_pDoc->m_pPG->m_fMoveInPage=TRUE;
m_pDoc->m_pPG->m_ptDrop=po.ptl;
return NOERROR;
}

//In CPage::DragDrop
if (m_pPG->m_fMoveInPage)
{
m_pTenantCur->Invalidate();

[Code to clip the rectangle to page boundaries]

m_pTenantCur->RectSet(&rcl, TRUE);
m_pTenantCur->Repaint();
return TRUE;
}

If m_fMoveInPage is not set after DoDragDrop, Patron knows that the end user is either copying a tenant on the same page or accepting a drop from something external. In either case, CDropTarget::Drop pastes a new tenant by calling CPatronDoc::PasteFromData—that manifestly useful function. PasteFromData doesn't know or care about the source of the data; it simply makes a new copy. Therefore, no special cases are needed to copy a tenant within the same page:


//In CDropTarget::Drop
m_pDoc->m_pPG->m_fMoveInPage=FALSE;
fRet=m_pDoc->FQueryPasteFromData(pIDataSource, &fe, &tType);

if (fRet)
{
po.fe=(m_pDoc->m_cf==fe.cfFormat) ? m_fe : fe;
fRet=m_pDoc->PasteFromData(pIDataSource, &fe, tType, &po, 0);
}

if (!fRet)
return ResultFromScode(E_FAIL);

*pdwEffect=DROPEFFECT_MOVE;

if (grfKeyState & MK_CONTROL)
*pdwEffect=DROPEFFECT_COPY;

return NOERROR;

Finally, CPage::DragDrop must delete the originally picked tenant if a move occurred to an external target. It accomplishes this by calling CPage::TenantDestroy:


if (DROPEFFECT_MOVE==dwEffect)
{
TenantDestroy();
return TRUE;
}

2 Some applications perform different actions on selected data depending on whether the end user quickly clicked the mouse button or clicked it and held it down for a little while. Microsoft Word 6 for example will start a drag and drop if you click and hold the mouse button in a block of selected text for 200 ms or so. If you click quickly it will remove the selection and place the text caret at the click point.