//+------------------------------------------------------------------------- // // Microsoft Windows // // Copyright (C) Microsoft Corporation, 2000 // // File: cplnkele.cpp // // This module implements a 'link' element in the Control Panel's DUI // view. Link elements have a title, infotip, icon and an associated // command that is invoked when the link is selected. The CLinkElement // object is an extension of the DUI::Button class. Direct UI automatically // creates an instance of CLinkElement when a 'linkelement' item from // cpview.ui is instantiated. // //-------------------------------------------------------------------------- #include "shellprv.h" #include "cpviewp.h" #include "cpaction.h" #include "cpduihlp.h" #include "cpguids.h" #include "cpuiele.h" #include "cplnkele.h" #include "cputil.h" #include "defviewp.h" #include "dobjutil.h" #include "ids.h" using namespace CPL; CLinkElement::CLinkElement( void ) : m_pUiCommand(NULL), m_eIconSize(eCPIMGSIZE(-1)), m_hwndInfotip(NULL), m_idTitle(0), m_idIcon(0), m_iDragState(DRAG_IDLE) { TraceMsg(TF_LIFE, "CLinkElement::CLinkElement, this = 0x%x", this); SetRect(&m_rcDragBegin, 0, 0, 0, 0); } CLinkElement::~CLinkElement( void ) { TraceMsg(TF_LIFE, "CLinkElement::~CLinkElement, this = 0x%x", this); _Destroy(); } // // This is called by the DUI engine when the link element is // created. // HRESULT CLinkElement::Create( // [static] DUI::Element **ppElement ) { HRESULT hr = E_OUTOFMEMORY; CLinkElement *ple = DUI::HNewAndZero(); if (NULL != ple) { hr = ple->_Initialize(); if (FAILED(hr)) { ple->Destroy(); ple = NULL; } } *ppElement = ple; return THR(hr); } // // This is called by the Control Panel UI code creating // the link element. // HRESULT CLinkElement::Initialize( IUICommand *pUiCommand, eCPIMGSIZE eIconSize ) { ASSERT(NULL == m_pUiCommand); ASSERT(NULL != pUiCommand); (m_pUiCommand = pUiCommand)->AddRef(); m_eIconSize = eIconSize; HRESULT hr = _CreateElementTitle(); if (SUCCEEDED(hr)) { // // We don't fail element creation if the icon // cannot be created. We want to display the // title without an icon so that we know there's // a problem retrieving the icon. // THR(_CreateElementIcon()); } // // Note that we don't fail element creation if accessibility // initialization fails. // THR(_InitializeAccessibility()); if (FAILED(hr)) { ATOMICRELEASE(m_pUiCommand); } return THR(hr); } HRESULT CLinkElement::_InitializeAccessibility( void ) { HRESULT hr = THR(SetAccessible(true)); if (SUCCEEDED(hr)) { hr = THR(SetAccRole(ROLE_SYSTEM_LINK)); if (SUCCEEDED(hr)) { LPWSTR pszTitle; hr = THR(_GetTitleText(&pszTitle)); if (SUCCEEDED(hr)) { hr = THR(SetAccName(pszTitle)); CoTaskMemFree(pszTitle); pszTitle = NULL; if (SUCCEEDED(hr)) { LPWSTR pszInfotip; hr = THR(_GetInfotipText(&pszInfotip)); if (SUCCEEDED(hr)) { hr = THR(SetAccDesc(pszInfotip)); CoTaskMemFree(pszInfotip); pszInfotip = NULL; if (SUCCEEDED(hr)) { TCHAR szDefAction[80]; if (0 < LoadString(HINST_THISDLL, IDS_CP_LINK_ACCDEFACTION, szDefAction, ARRAYSIZE(szDefAction))) { hr = THR(SetAccDefAction(szDefAction)); } else { hr = THR(ResultFromLastError()); } } } } } } } return THR(hr); } void CLinkElement::OnDestroy( void ) { _Destroy(); DUI::Button::OnDestroy(); } void CLinkElement::OnInput( DUI::InputEvent *pev ) { if (GINPUT_MOUSE == pev->nDevice) { // // Use a set of states to control our handling of // the mouse inputs for drag/drop. // // DRAG_IDLE - We have not yet detected any drag activity. // DRAG_HITTESTING - Waiting to see if user drags cursor a minimum distance. // DRAG_DRAGGING - User did drag cursor a minimum distance and we're now // inside the drag loop. // // // START -+-> DRAG_IDLE --> [ GMOUSE_DRAG ] --> DRAG_HITTESTING --+ // | | // | [ GMOUSE_DRAG + | // | moved SM_CXDRAG | // | or SM_CYDRAG ] | // | | // +-<--------------- [ GMOUSE_UP ] <--- DRAG_DRAGGING <---+ // DUI::MouseEvent *pmev = (DUI::MouseEvent *)pev; switch(pev->nCode) { case GMOUSE_UP: m_iDragState = DRAG_IDLE; break; case GMOUSE_DRAG: switch(m_iDragState) { case DRAG_IDLE: { // // This is the same way comctl's listview calculates // the begin-drag rect. // int dxClickRect = GetSystemMetrics(SM_CXDRAG); int dyClickRect = GetSystemMetrics(SM_CYDRAG); if (4 > dxClickRect) { dxClickRect = dyClickRect = 4; } // // Remember where the mouse pointer is on our first // indication that a drag operation is starting. // SetRect(&m_rcDragBegin, pmev->ptClientPxl.x - dxClickRect, pmev->ptClientPxl.y - dyClickRect, pmev->ptClientPxl.x + dxClickRect, pmev->ptClientPxl.y + dyClickRect); m_iDragState = DRAG_HITTESTING; break; } case DRAG_HITTESTING: if (!PtInRect(&m_rcDragBegin, pmev->ptClientPxl)) { // // Begin the drag/drop operation only if we've moved the mouse // outside the "drag begin" rectangle. This prevents us from // confusing a normal click with a drag/drop operation. // m_iDragState = DRAG_DRAGGING; // // Position the drag point at the middle of the item's image. // UINT cxIcon = 32; UINT cyIcon = 32; CPL::ImageDimensionsFromDesiredSize(m_eIconSize, &cxIcon, &cyIcon); _BeginDrag(cxIcon / 2, cyIcon / 2); } break; case DRAG_DRAGGING: break; } break; default: break; } } Button::OnInput(pev); } void CLinkElement::OnEvent( DUI::Event *pev ) { if (DUI::Button::Click == pev->uidType) { pev->fHandled = true; DUI::ButtonClickEvent * pbe = (DUI::ButtonClickEvent *) pev; if (1 != pbe->nCount) { return; // ingore additional clicks - don't forward. } _OnSelected(); } else if (DUI::Button::Context == pev->uidType) { DUI::ButtonContextEvent *peButton = reinterpret_cast(pev); _OnContextMenu(peButton); pev->fHandled = true; } Button::OnEvent(pev); } void CLinkElement::OnPropertyChanged( DUI::PropertyInfo *ppi, int iIndex, DUI::Value *pvOld, DUI::Value *pvNew ) { // // Don't trace this function. It's called very often. // // Perform default processing. // Button::OnPropertyChanged(ppi, iIndex, pvOld, pvNew); if (IsProp(MouseWithin)) { _OnMouseOver(pvNew); } } // // Called to begin a drag-drop operation from the control panel. // This is used for dragging CPL applet icons to shell folders // for shortcut creation. // HRESULT CLinkElement::_BeginDrag( int iClickPosX, int iClickPosY ) { DBG_ENTER(FTF_CPANEL, "CLinkElement::_BeginDrag"); HRESULT hr = E_FAIL; HRESULT hrCoInit = SHCoInitialize(); if (SUCCEEDED(hrCoInit)) { hr = hrCoInit; IDataObject *pdtobj; hr = _GetDragDropData(&pdtobj); if (SUCCEEDED(hr)) { // // Ignore any failure to set the drag image. Drag images // are not supported on some video configurations. // In these cases, we still want to be able to create a shortcut. // THR(_SetDragImage(pdtobj, iClickPosX, iClickPosY)); HWND hwndRoot; hr = THR(Dui_GetElementRootHWND(this, &hwndRoot)); if (SUCCEEDED(hr)) { DWORD dwEffect = DROPEFFECT_LINK; hr = THR(SHDoDragDrop(hwndRoot, pdtobj, NULL, dwEffect, &dwEffect)); } pdtobj->Release(); } SHCoUninitialize(hrCoInit); } DBG_EXIT_HRES(FTF_CPANEL, "CLinkElement::_BeginDrag", hr); return THR(hr); } // // Get and prepare the data object used in a drag-drop operation. // The returned data object is suitable for use by SHDoDragDrop. // HRESULT CLinkElement::_GetDragDropData( IDataObject **ppdtobj ) { DBG_ENTER(FTF_CPANEL, "CLinkElement::_GetDragDropData"); ASSERT(NULL != ppdtobj); ASSERT(!IsBadWritePtr(ppdtobj, sizeof(*ppdtobj))); ASSERT(NULL != m_pUiCommand); *ppdtobj = NULL; ICpUiCommand *puic; HRESULT hr = m_pUiCommand->QueryInterface(IID_PPV_ARG(ICpUiCommand, &puic)); if (SUCCEEDED(hr)) { // // Note that this call will fail with E_NOTIMPL for links that don't // provide drag-drop data. Only CPL applet links provide data. // This is how we limit drag-drop to only CPL applets. // IDataObject *pdtobj; hr = THR(puic->GetDataObject(&pdtobj)); if (SUCCEEDED(hr)) { hr = _SetPreferredDropEffect(pdtobj, DROPEFFECT_LINK); if (SUCCEEDED(hr)) { (*ppdtobj = pdtobj)->AddRef(); } pdtobj->Release(); } puic->Release(); } DBG_EXIT_HRES(FTF_CPANEL, "CLinkElement::_GetDragDropData", hr); return THR(hr); } HRESULT CLinkElement::_SetPreferredDropEffect( IDataObject *pdtobj, DWORD dwEffect ) { DBG_ENTER(FTF_CPANEL, "CLinkElement::_SetPreferredDropEffect"); HRESULT hr = S_OK; static CLIPFORMAT cf; if ((CLIPFORMAT)0 == cf) { cf = (CLIPFORMAT)RegisterClipboardFormat(CFSTR_PREFERREDDROPEFFECT); if ((CLIPFORMAT)0 == cf) { hr = THR(ResultFromLastError()); } } if (SUCCEEDED(hr)) { hr = THR(DataObj_SetDWORD(pdtobj, cf, dwEffect)); } DBG_EXIT_HRES(FTF_CPANEL, "CLinkElement::_SetPreferredDropEffect", hr); return THR(hr); } // // Set up the drag image in the data object so that our icon is // displayed during the drag operation. // // I took this code from the old webvw project's fldricon.cpp // implementation (shell\ext\webvw\fldricon.cpp). It seems to // work just fine. // HRESULT CLinkElement::_SetDragImage( IDataObject *pdtobj, int iClickPosX, int iClickPosY ) { DBG_ENTER(FTF_CPANEL, "CLinkElement::_SetDragImage"); ASSERT(NULL != pdtobj); HRESULT hr = S_OK; HDC hdc = CreateCompatibleDC(NULL); if (NULL == hdc) { hr = THR(ResultFromLastError()); } else { HBITMAP hbm; LONG lBitmapWidth; LONG lBitmapHeight; hr = _GetDragImageBitmap(&hbm, &lBitmapWidth, &lBitmapHeight); if (SUCCEEDED(hr)) { IDragSourceHelper *pdsh; hr = CoCreateInstance(CLSID_DragDropHelper, NULL, CLSCTX_INPROC_SERVER, IID_PPV_ARG(IDragSourceHelper, &pdsh)); if (SUCCEEDED(hr)) { BITMAPINFOHEADER bmi = {0}; BITMAP bm = {0}; UINT uBufferOffset = 0; // // This is a screwy procedure to use GetDIBits. // See knowledge base Q80080 // if (GetObject(hbm, sizeof(BITMAP), &bm)) { bmi.biSize = sizeof(BITMAPINFOHEADER); bmi.biWidth = bm.bmWidth; bmi.biHeight = bm.bmHeight; bmi.biPlanes = 1; bmi.biBitCount = bm.bmPlanes * bm.bmBitsPixel; // // This needs to be one of these 4 values // if (bmi.biBitCount <= 1) bmi.biBitCount = 1; else if (bmi.biBitCount <= 4) bmi.biBitCount = 4; else if (bmi.biBitCount <= 8) bmi.biBitCount = 8; else bmi.biBitCount = 24; bmi.biCompression = BI_RGB; // // Total size of buffer for info struct and color table // uBufferOffset = sizeof(BITMAPINFOHEADER) + ((bmi.biBitCount == 24) ? 0 : ((1 << bmi.biBitCount) * sizeof(RGBQUAD))); // // Buffer for bitmap bits, so we can copy them. // BYTE *psBits = (BYTE *)SHAlloc(uBufferOffset); if (NULL == psBits) { hr = THR(E_OUTOFMEMORY); } else { // // Put bmi into the memory block // CopyMemory(psBits, &bmi, sizeof(BITMAPINFOHEADER)); // // Get the size of the buffer needed for bitmap bits // if (!GetDIBits(hdc, hbm, 0, 0, NULL, (BITMAPINFO *) psBits, DIB_RGB_COLORS)) { hr = THR(ResultFromLastError()); } else { // // Realloc our buffer to be big enough // psBits = (BYTE *)SHRealloc(psBits, uBufferOffset + ((BITMAPINFOHEADER *) psBits)->biSizeImage); if (NULL == psBits) { hr = THR(E_OUTOFMEMORY); } else { // // Fill the buffer // if (!GetDIBits(hdc, hbm, 0, bmi.biHeight, (void *)(psBits + uBufferOffset), (BITMAPINFO *)psBits, DIB_RGB_COLORS)) { hr = THR(ResultFromLastError()); } else { SHDRAGIMAGE shdi; // Drag images struct shdi.hbmpDragImage = CreateBitmapIndirect(&bm); if (NULL == shdi.hbmpDragImage) { hr = THR(ResultFromLastError()); } else { // // Set the drag image bitmap // if (SetDIBits(hdc, shdi.hbmpDragImage, 0, lBitmapHeight, (void *)(psBits + uBufferOffset), (BITMAPINFO *)psBits, DIB_RGB_COLORS)) { // // Populate the drag image structure // shdi.sizeDragImage.cx = lBitmapWidth; shdi.sizeDragImage.cy = lBitmapHeight; shdi.ptOffset.x = iClickPosX; shdi.ptOffset.y = iClickPosY; shdi.crColorKey = 0; // // Set the drag image // hr = pdsh->InitializeFromBitmap(&shdi, pdtobj); } else { hr = THR(ResultFromLastError()); } if (FAILED(hr)) { DeleteObject(shdi.hbmpDragImage); } } } } } if (NULL != psBits) { SHFree(psBits); } } } pdsh->Release(); } DeleteObject(hbm); } DeleteDC(hdc); } DBG_EXIT_HRES(FTF_CPANEL, "CLinkElement::_SetDragImage", hr); return THR(hr); } HRESULT CLinkElement::_GetDragImageBitmap( HBITMAP *phbm, LONG *plWidth, LONG *plHeight ) { DBG_ENTER(FTF_CPANEL, "CLinkElement::_GetDragImageBitmap"); ASSERT(NULL != phbm); ASSERT(!IsBadWritePtr(phbm, sizeof(*phbm))); ASSERT(NULL != plWidth); ASSERT(!IsBadWritePtr(plWidth, sizeof(*plWidth))); ASSERT(NULL != plHeight); ASSERT(!IsBadWritePtr(plHeight, sizeof(*plHeight))); *phbm = NULL; *plWidth = 0; *plHeight = 0; HICON hIcon; HRESULT hr = _GetElementIcon(&hIcon); if (SUCCEEDED(hr)) { ICONINFO iconinfo; if (GetIconInfo(hIcon, &iconinfo)) { BITMAP bm; if (GetObject(iconinfo.hbmColor, sizeof(bm), &bm)) { *plWidth = bm.bmWidth; *plHeight = bm.bmHeight; *phbm = iconinfo.hbmColor; } else { DeleteObject(iconinfo.hbmColor); hr = THR(ResultFromLastError()); } DeleteObject(iconinfo.hbmMask); } else { hr = THR(ResultFromLastError()); } DestroyIcon(hIcon); } DBG_EXIT_HRES(FTF_CPANEL, "CLinkElement::_GetDragImageBitmap", hr); return THR(hr); } HRESULT CLinkElement::_Initialize( void ) { HRESULT hr = Button::Initialize(AE_Mouse | AE_Keyboard); if (SUCCEEDED(hr)) { hr = _AddOrDeleteAtoms(true); } return THR(hr); } void CLinkElement::_Destroy( void ) { if (NULL != m_hwndInfotip && IsWindow(m_hwndInfotip)) { SHDestroyInfotipWindow(&m_hwndInfotip); } ATOMICRELEASE(m_pUiCommand); _AddOrDeleteAtoms(false); } HRESULT CLinkElement::_AddOrDeleteAtoms( bool bAdd ) { struct CPL::ATOMINFO rgAtomInfo[] = { { L"title", &m_idTitle }, { L"icon", &m_idIcon }, }; HRESULT hr = Dui_AddOrDeleteAtoms(rgAtomInfo, ARRAYSIZE(rgAtomInfo), bAdd); return THR(hr); } HRESULT CLinkElement::_CreateElementTitle( void ) { LPWSTR pszTitle; HRESULT hr = _GetTitleText(&pszTitle); if (SUCCEEDED(hr)) { hr = Dui_SetDescendentElementText(this, L"title", pszTitle); CoTaskMemFree(pszTitle); } return THR(hr); } HRESULT CLinkElement::_CreateElementIcon( void ) { HICON hIcon; HRESULT hr = _GetElementIcon(&hIcon); if (SUCCEEDED(hr)) { hr = Dui_SetDescendentElementIcon(this, L"icon", hIcon); if (FAILED(hr)) { DestroyIcon(hIcon); } } return THR(hr); } HRESULT CLinkElement::_GetElementIcon( HICON *phIcon ) { ASSERT(NULL != phIcon); ASSERT(!IsBadWritePtr(phIcon, sizeof(*phIcon))); ASSERT(NULL != m_pUiCommand); *phIcon = NULL; ICpUiElementInfo *pei; HRESULT hr = m_pUiCommand->QueryInterface(IID_PPV_ARG(ICpUiElementInfo, &pei)); if (SUCCEEDED(hr)) { hr = pei->LoadIcon(m_eIconSize, phIcon); pei->Release(); } return THR(hr); } HRESULT CLinkElement::_OnContextMenu( DUI::ButtonContextEvent *peButton ) { DBG_ENTER(FTF_CPANEL, "CLinkElement::_OnContextMenu"); ICpUiCommand *pcmd; HRESULT hr = m_pUiCommand->QueryInterface(IID_PPV_ARG(ICpUiCommand, &pcmd)); if (SUCCEEDED(hr)) { HWND hwndRoot; hr = Dui_GetElementRootHWND(this, &hwndRoot); if (SUCCEEDED(hr)) { if (-1 == peButton->pt.x) { // // Keyboard context menu. // SIZE size; hr = Dui_GetElementExtent(this, &size); if (SUCCEEDED(hr)) { peButton->pt.x = size.cx / 2; peButton->pt.y = size.cy / 2; } } POINT pt; hr = Dui_MapElementPointToRootHWND(this, peButton->pt, &pt); if (SUCCEEDED(hr)) { if (ClientToScreen(hwndRoot, &pt)) { // // InvokeContextMenu returns S_FALSE if the command doesn't // provide a context menu. // hr = pcmd->InvokeContextMenu(hwndRoot, &pt); } else { hr = CPL::ResultFromLastError(); } } } pcmd->Release(); } else if (E_NOINTERFACE == hr) { hr = S_FALSE; } DBG_EXIT_HRES(FTF_CPANEL, "CLinkElement::_OnContextMenu", hr); return THR(hr); } HRESULT CLinkElement::_OnSelected( void ) { ASSERT(NULL != m_pUiCommand); // // Delay navigation until double-click time times out occurs. // // KB: gpease 05-APR-2001 Fix for Whistler Bug #338552 (and others) // // Delaying this prevents the "second click" from being applied // to the newly navigated frame. Previously, if there happen to // be a new link in the new frame at the same mouse point at // which the previous navigation occured, the new link would have // received the 2nd click and we'd navigate that link as well. This // causes the current frame to get the 2nd click which we ignore // since we only care about the single click (see OnEvent above). // HWND hwndRoot; HRESULT hr = Dui_GetElementRootHWND(this, &hwndRoot); if (SUCCEEDED(hr)) { SendMessage(hwndRoot, WM_USER_DELAY_NAVIGATION, (WPARAM) NULL, (LPARAM) m_pUiCommand); } return THR(hr); } void CLinkElement::_OnMouseOver( DUI::Value *pvNewMouseWithin ) { _ShowInfotipWindow(pvNewMouseWithin->GetBool()); } // // Retrieve the title text for the element. // Caller must free returned buffer using CoTaskMemFree. // HRESULT CLinkElement::_GetTitleText( LPWSTR *ppszTitle ) { ASSERT(NULL != m_pUiCommand); ASSERT(NULL != ppszTitle); ASSERT(!IsBadWritePtr(ppszTitle, sizeof(*ppszTitle))); *ppszTitle = NULL; ICpUiElementInfo *pei; HRESULT hr = m_pUiCommand->QueryInterface(IID_PPV_ARG(ICpUiElementInfo, &pei)); if (SUCCEEDED(hr)) { hr = pei->LoadName(ppszTitle); pei->Release(); } return THR(hr); } // // Retrieve the infotip text for the element. // Caller must free returned buffer using CoTaskMemFree. // HRESULT CLinkElement::_GetInfotipText( LPWSTR *ppszInfotip ) { ASSERT(NULL != m_pUiCommand); ASSERT(NULL != ppszInfotip); ASSERT(!IsBadWritePtr(ppszInfotip, sizeof(*ppszInfotip))); *ppszInfotip = NULL; ICpUiElementInfo *pei; HRESULT hr = m_pUiCommand->QueryInterface(IID_PPV_ARG(ICpUiElementInfo, &pei)); if (SUCCEEDED(hr)) { hr = pei->LoadTooltip(ppszInfotip); pei->Release(); } return THR(hr); } HRESULT CLinkElement::_ShowInfotipWindow( bool bShow ) { HRESULT hr = S_OK; if (bShow) { if (NULL == m_hwndInfotip) { HWND hwndRoot; hr = THR(Dui_GetElementRootHWND(this, &hwndRoot)); if (SUCCEEDED(hr)) { LPWSTR pszInfotip; hr = THR(_GetInfotipText(&pszInfotip)); if (SUCCEEDED(hr)) { hr = THR(SHCreateInfotipWindow(hwndRoot, pszInfotip, &m_hwndInfotip)); CoTaskMemFree(pszInfotip); } } } if (SUCCEEDED(hr)) { hr = THR(SHShowInfotipWindow(m_hwndInfotip, TRUE)); } } else { if (NULL != m_hwndInfotip) { hr = THR(SHDestroyInfotipWindow(&m_hwndInfotip)); } } return THR(hr); } // // ClassInfo (must appear after property definitions). // DUI::IClassInfo *CLinkElement::Class = NULL; HRESULT CLinkElement::Register() { return DUI::ClassInfo::Register(L"linkelement", NULL, 0); }