tinyphone/microsip/ClosableTabCtrl.cpp
2018-11-21 15:32:29 +05:30

581 lines
17 KiB
C++

#include "stdafx.h"
#include "mainDlg.h"
#include "ClosableTabCtrl.h"
#include "VisualStylesXP.h"
#ifdef _DEBUG
#define new DEBUG_NEW
#undef THIS_FILE
static char THIS_FILE[] = __FILE__;
#endif
// _WIN32_WINNT >= 0x0501 (XP only)
#define _WM_THEMECHANGED 0x031A
#define _ON_WM_THEMECHANGED() \
{ _WM_THEMECHANGED, 0, 0, 0, AfxSig_l, \
(AFX_PMSG)(AFX_PMSGW) \
(static_cast< LRESULT (AFX_MSG_CALL CWnd::*)(void) > (_OnThemeChanged)) \
},
///////////////////////////////////////////////////////////////////////////////
// CClosableTabCtrl
IMPLEMENT_DYNAMIC(CClosableTabCtrl, CTabCtrl)
BEGIN_MESSAGE_MAP(CClosableTabCtrl, CTabCtrl)
ON_WM_LBUTTONUP()
ON_WM_LBUTTONDOWN()
ON_WM_LBUTTONDBLCLK()
ON_WM_MBUTTONUP()
ON_WM_CREATE()
ON_WM_SYSCOLORCHANGE()
ON_WM_CONTEXTMENU()
_ON_WM_THEMECHANGED()
ON_WM_CTLCOLOR_REFLECT()
ON_WM_CTLCOLOR()
ON_WM_ERASEBKGND()
ON_WM_MEASUREITEM()
ON_WM_MEASUREITEM_REFLECT()
END_MESSAGE_MAP()
CClosableTabCtrl::CClosableTabCtrl()
{
m_bCloseable = true;
memset(&m_iiCloseButton, 0, sizeof m_iiCloseButton);
m_ptCtxMenu.SetPoint(-1, -1);
iTabClose = -1;
}
CClosableTabCtrl::~CClosableTabCtrl()
{
}
void CClosableTabCtrl::GetCloseButtonRect(int iItem, const CRect& rcItem, CRect& rcCloseButton, bool bItemSelected, bool bVistaThemeActive)
{
rcCloseButton.top = rcItem.top + 2;
rcCloseButton.bottom = rcCloseButton.top + (m_iiCloseButton.rcImage.bottom - m_iiCloseButton.rcImage.top);
rcCloseButton.right = rcItem.right - 2;
rcCloseButton.left = rcCloseButton.right - (m_iiCloseButton.rcImage.right - m_iiCloseButton.rcImage.left);
if (bVistaThemeActive)
rcCloseButton.left -= 1; // the close button does not look 'symetric' with a width of 16, give it 17
if (bItemSelected) {
rcCloseButton.OffsetRect(-1, 0);
if (bVistaThemeActive) {
int iItems = GetItemCount();
if (iItems > 1 && iItem == iItems - 1)
rcCloseButton.OffsetRect(-2, 0);
}
}
else {
if (bVistaThemeActive) {
int iItems = GetItemCount();
if (iItems > 1 && iItem < iItems - 1)
rcCloseButton.OffsetRect(2, 0);
}
}
}
int CClosableTabCtrl::GetTabUnderPoint(CPoint point) const
{
int iTabs = GetItemCount();
for (int i = 0; i < iTabs; i++)
{
CRect rcItem;
GetItemRect(i, rcItem);
rcItem.InflateRect(2, 2); // get the real tab item rect
if (rcItem.PtInRect(point))
return i;
}
return -1;
}
int CClosableTabCtrl::GetTabUnderContextMenu() const
{
if (m_ptCtxMenu.x == -1 || m_ptCtxMenu.y == -1)
return -1;
return GetTabUnderPoint(m_ptCtxMenu);
}
bool CClosableTabCtrl::SetDefaultContextMenuPos()
{
int iTab = GetCurSel();
if (iTab != -1)
{
CRect rcItem;
if (GetItemRect(iTab, &rcItem))
{
rcItem.InflateRect(2, 2); // get the real tab item rect
m_ptCtxMenu.x = rcItem.left + rcItem.Width()/2;
m_ptCtxMenu.y = rcItem.top + rcItem.Height()/2;
return true;
}
}
return false;
}
void CClosableTabCtrl::OnMButtonUp(UINT nFlags, CPoint point)
{
if (m_bCloseable)
{
int iTab = GetTabUnderPoint(point);
if (iTab != -1) {
GetParent()->SendMessage(UM_CLOSETAB, (WPARAM)iTab);
return;
}
}
CTabCtrl::OnMButtonUp(nFlags, point);
}
void CClosableTabCtrl::OnLButtonUp(UINT nFlags, CPoint point)
{
if (iTabClose != -1) {
GetParent()->SendMessage(UM_CLOSETAB, (WPARAM)iTabClose);
iTabClose = -1;
} else {
CTabCtrl::OnLButtonUp(nFlags, point);
}
}
void CClosableTabCtrl::OnLButtonDown(UINT nFlags, CPoint point)
{
if (m_bCloseable)
{
int iTab = GetTabUnderPoint(point);
if (iTab != -1)
{
CRect rcItem;
GetItemRect(iTab, rcItem);
rcItem.InflateRect(2, 2); // get the real tab item rect
bool bVistaThemeActive = IsVistaThemeActive();
CRect rcCloseButton;
GetCloseButtonRect(iTab, rcItem, rcCloseButton, iTab == GetCurSel(), bVistaThemeActive);
// The visible part of our close icon is one pixel less on each side
if (!bVistaThemeActive) {
rcCloseButton.top += 1;
rcCloseButton.left += 1;
rcCloseButton.right -= 1;
rcCloseButton.bottom -= 1;
}
if (rcCloseButton.PtInRect(point)) {
iTabClose = iTab;
return;
}
}
}
CTabCtrl::OnLButtonDown(nFlags, point);
}
void CClosableTabCtrl::OnLButtonDblClk(UINT nFlags, CPoint point)
{
int iTab = GetTabUnderPoint(point);
if (iTab != -1) {
GetParent()->SendMessage(UM_DBLCLICKTAB, (WPARAM)iTab);
return;
}
CTabCtrl::OnLButtonDblClk(nFlags, point);
}
// It would be nice if there would the option to restrict the maximum width of a tab control.
// We would need that feature actually for almost all our tab controls. Especially for the
// search results list - those tab control labels can get quite large. But I did not yet a
// find a way to limit the width of tabs. Although MSDN says that an owner drawn
// tab control receives a WM_MEASUREITEM, I never got one.
// Vista: This gets never called for an owner drawn tab control
void CClosableTabCtrl::OnMeasureItem(int iCtlId, LPMEASUREITEMSTRUCT lpMeasureItemStruct)
{
TRACE("CClosableTabCtrl::OnMeasureItem\n");
__super::OnMeasureItem(iCtlId, lpMeasureItemStruct);
}
// Vista: This gets never called for an owner drawn tab control
void CClosableTabCtrl::MeasureItem(LPMEASUREITEMSTRUCT)
{
TRACE("CClosableTabCtrl::MeasureItem\n");
}
void CClosableTabCtrl::DrawItem(LPDRAWITEMSTRUCT lpDIS)
{
CRect rect(lpDIS->rcItem);
int nTabIndex = lpDIS->itemID;
if (nTabIndex < 0)
return;
TCHAR szLabel[256];
TC_ITEM tci;
tci.mask = TCIF_TEXT | TCIF_IMAGE | TCIF_STATE;
tci.pszText = szLabel;
tci.cchTextMax = _countof(szLabel);
tci.dwStateMask = TCIS_HIGHLIGHTED;
if (!GetItem(nTabIndex, &tci))
return;
szLabel[_countof(szLabel) - 1] = _T('\0');
//TRACE("CClosableTabCtrl::DrawItem: item=%u, state=%08x, color=%08x, rc=%3d,%3d,%3dx%3d\n", nTabIndex, tci.dwState, GetTextColor(lpDIS->hDC), lpDIS->rcItem.left, lpDIS->rcItem.top, lpDIS->rcItem.right - lpDIS->rcItem.left, lpDIS->rcItem.bottom - lpDIS->rcItem.top);
CDC* pDC = CDC::FromHandle(lpDIS->hDC);
if (!pDC)
return;
CRect rcFullItem(lpDIS->rcItem);
bool bSelected = (lpDIS->itemState & ODS_SELECTED) != 0;
///////////////////////////////////////////////////////////////////////////////////////
// Adding support for XP Styles (Vista Themes) for owner drawn tab controls simply
// does *not* work under Vista. Maybe it works under XP (did not try), but that is
// meaningless because under XP a owner drawn tab control is already rendered *with*
// the proper XP Styles. So, for XP there is no need to care about the theme API at all.
//
// However, under Vista, a tab control which has the TCS_OWNERDRAWFIXED
// style gets additional 3D-borders which are applied by Vista *after* WM_DRAWITEM
// was processed. Thus, there is no known workaround available to prevent Vista from
// adding those old fashioned 3D-borders. We can render the tab control items within
// the WM_DRAWITEM handler in whatever style we want, but Vista will in each case
// overwrite the borders of each tab control item with old fashioned 3D-borders...
//
// To complete this experience, tab controls also do not support NMCUSTOMDRAW. So, the
// only known way to customize a tab control is by using TCS_OWNERDRAWFIXED which does
// however not work properly under Vista.
//
// The "solution" which is currently implemented to prevent Vista from drawing those
// 3D-borders is by using "ExcludeClipRect" to reduce the drawing area which is used
// by Windows after WM_DRAWITEM was processed. This "solution" is very sensitive to
// the used rectangles and offsets in general. Incrementing/Decrementing one of the
// "rcItem", "rcFullItem", etc. rectangles makes the entire "solution" flawed again
// because some borders would become visible again.
//
HTHEME hTheme = NULL;
int iPartId = TABP_TABITEM;
int iStateId = TIS_NORMAL;
bool bVistaHotTracked = false;
bool bVistaThemeActive = IsVistaThemeActive();
if (bVistaThemeActive)
{
// To determine if the current item is in 'hot tracking' mode, we need to evaluate
// the current foreground color - there is no flag which would indicate this state
// more safely. This applies only for Vista and for tab controls which have the
// TCS_OWNERDRAWFIXED style.
bVistaHotTracked = pDC->GetTextColor() == GetSysColor(COLOR_HOTLIGHT);
hTheme = g_xpStyle.OpenThemeData(m_hWnd, L"TAB");
if (hTheme)
{
if (bSelected) {
// get the real tab item rect
rcFullItem.left += 1;
rcFullItem.right -= 1;
rcFullItem.bottom -= 1;
}
else
rcFullItem.InflateRect(2, 2); // get the real tab item rect
CRect rcBk(rcFullItem);
if (bSelected)
{
iStateId = TTIS_SELECTED;
if (nTabIndex == 0) {
// First item
if (nTabIndex == GetItemCount() - 1)
iPartId = TABP_TOPTABITEMBOTHEDGE; // First & Last item
else
iPartId = TABP_TOPTABITEMLEFTEDGE;
}
else if (nTabIndex == GetItemCount() - 1) {
// Last item
iPartId = TABP_TOPTABITEMRIGHTEDGE;
}
else {
iPartId = TABP_TOPTABITEM;
}
}
else
{
rcBk.top += 2;
iStateId = bVistaHotTracked ? TIS_HOT : TIS_NORMAL;
if (nTabIndex == 0) {
// First item
if (nTabIndex == GetItemCount() - 1)
iPartId = TABP_TABITEMBOTHEDGE; // First & Last item
else
iPartId = TABP_TABITEMLEFTEDGE;
}
else if (nTabIndex == GetItemCount() - 1) {
// Last item
iPartId = TABP_TABITEMRIGHTEDGE;
}
else {
iPartId = TABP_TABITEM;
}
}
if (g_xpStyle.IsThemeBackgroundPartiallyTransparent(hTheme, iPartId, iStateId))
g_xpStyle.DrawThemeParentBackground(m_hWnd, *pDC, &rcFullItem);
g_xpStyle.DrawThemeBackground(hTheme, *pDC, iPartId, iStateId, &rcBk, NULL);
}
}
// Following background clearing is needed for:
// WinXP/Vista (when used without an application theme)
// Vista (when used with an application theme but without a theme for the tab control)
if ( (!g_xpStyle.IsThemeActive() || !g_xpStyle.IsAppThemed())
|| (hTheme == NULL && bVistaThemeActive) ) {
CRect rectFill;
rectFill.left = rect.left+1;
rectFill.top = rect.top+1;
rectFill.right = rect.right-1;
rectFill.bottom = rect.bottom;
COLORREF clref = ::GetSysColor(COLOR_BTNFACE);
if (bSelected) {
BYTE r = GetRValue(clref);
BYTE g = GetGValue(clref);
BYTE b = GetBValue(clref);
if (r==g && g==b && b==255) {
clref = RGB(224, 224, 224);
} else {
clref = RGB(255, 255, 255);
}
}
pDC->FillSolidRect(rectFill, clref);
}
int iOldBkMode = pDC->SetBkMode(TRANSPARENT);
// Draw image on left side
CImageList *piml = GetImageList();
if (tci.iImage >= 0 && piml && piml->m_hImageList)
{
IMAGEINFO ii;
piml->GetImageInfo(0, &ii);
rect.left += bSelected ? 8 : 4;
piml->Draw(pDC, tci.iImage, CPoint(rect.left, rect.top + (bSelected?2:1)), ILD_TRANSPARENT);
rect.left += (ii.rcImage.right - ii.rcImage.left);
if (!bSelected)
rect.left += 4;
}
bool bCloseable = m_bCloseable;
if (bCloseable && GetParent()->SendMessage(UM_QUERYTAB, nTabIndex))
bCloseable = false;
// Draw 'Close button' at right side
if (bCloseable && m_ImgLstCloseButton.m_hImageList)
{
CRect rcCloseButton;
GetCloseButtonRect(nTabIndex, rect, rcCloseButton, bSelected, bVistaThemeActive);
HTHEME hThemeNC = bVistaThemeActive ? g_xpStyle.OpenThemeData(m_hWnd, L"WINDOW") : NULL;
if (hThemeNC) {
// Possible "Close" parts: WP_CLOSEBUTTON, WP_SMALLCLOSEBUTTON, WP_MDICLOSEBUTTON
int iPartId = WP_SMALLCLOSEBUTTON;
int iStateId = (bSelected || bVistaHotTracked) ? CBS_NORMAL : CBS_DISABLED;
if (g_xpStyle.IsThemeBackgroundPartiallyTransparent(hTheme, iPartId, iStateId))
g_xpStyle.DrawThemeParentBackground(m_hWnd, *pDC, &rcCloseButton);
g_xpStyle.DrawThemeBackground(hThemeNC, *pDC, iPartId, iStateId, rcCloseButton, NULL);
g_xpStyle.CloseThemeData(hThemeNC);
}
else {
m_ImgLstCloseButton.Draw(pDC, (bSelected || bVistaHotTracked) ? 0 : 1, rcCloseButton.TopLeft(), ILD_TRANSPARENT);
}
rect.right = rcCloseButton.left - 2;
if (bSelected)
rect.left += hTheme ? 4 : 2;
}
COLORREF crOldColor = CLR_NONE;
if (tci.dwState & TCIS_HIGHLIGHTED)
crOldColor = pDC->SetTextColor(RGB(192, 0, 0));
else if (bVistaHotTracked)
crOldColor = pDC->SetTextColor(GetSysColor(COLOR_BTNTEXT));
rect.top += bSelected ? 4 : 3;
// Vista: Tab control has troubles with determining the width of a tab if the
// label contains one '&' character. To get around this, we use the old code which
// replaces one '&' character with two '&' characters and we do not specify DT_NOPREFIX
// here when drawing the text.
//
// Vista: "DrawThemeText" can not be used in case we need a certain foreground color. Thus we always us
// "DrawText" to always get the same font and metrics (just for safety).
pDC->DrawText(szLabel, rect, DT_SINGLELINE | DT_TOP | DT_CENTER /*| DT_NOPREFIX*/);
if (crOldColor != CLR_NONE)
pDC->SetTextColor(crOldColor);
pDC->SetBkMode(iOldBkMode);
if (hTheme)
{
CRect rcClip(rcFullItem);
if (bSelected) {
rcClip.left -= 2 + 1;
rcClip.right += 2 + 1;
}
else {
rcClip.top += 2;
}
pDC->ExcludeClipRect(&rcClip);
g_xpStyle.CloseThemeData(hTheme);
}
}
void CClosableTabCtrl::PreSubclassWindow()
{
CTabCtrl::PreSubclassWindow();
InternalInit();
}
int CClosableTabCtrl::OnCreate(LPCREATESTRUCT lpCreateStruct)
{
if (CTabCtrl::OnCreate(lpCreateStruct) == -1)
return -1;
InternalInit();
return 0;
}
void CClosableTabCtrl::InternalInit()
{
ModifyStyle(0, TCS_OWNERDRAWFIXED);
#if 1
// Under Vista Aero, all tab controls get by default the TCS_HOTTRACK
// style even if it was not specified within the resource file. Though, to 'see'
// the hot tracking effect the control also need to get initialized explicitly with
// the WS_CLIPCHILDREN style within a *seperate* function call. Yes, there is no
// logic to all this, not at all. It simply is that way.
//
// So, do *not* "optimize" that code by using only one "ModifyStyle" function call.
// The 2nd function call to "ModifyStyle" is very much by intention!
//
// However, the hot tracking effect which is achived this way does not survive a
// theme change. After the theme is changed (regardless whether we switch between
// Vista themes or from/to a non-Vista theme), the hot tracking effect is gone even
// if we try to modify the styles again within OnThemeChanged...
if (IsVistaThemeActive())
ModifyStyle(0, WS_CLIPCHILDREN);
#else
// Remove the automatically applied hot tracking effect to avoid that the tab control
// may use it when it also sets the WS_CLIPCHILDREN (for other reasons) later.
ModifyStyle(TCS_HOTTRACK, 0);
#endif
SetAllIcons();
}
void CClosableTabCtrl::OnSysColorChange()
{
CTabCtrl::OnSysColorChange();
SetAllIcons();
}
void CClosableTabCtrl::SetAllIcons()
{
if (m_bCloseable)
{
const int iIconWidth = 16;
const int iIconHeight = 16;
m_ImgLstCloseButton.DeleteImageList();
m_ImgLstCloseButton.Create(iIconWidth, iIconHeight, ILC_COLOR32 | ILC_MASK, 0, 1);
m_ImgLstCloseButton.Add(AfxGetApp()->LoadIcon(IDI_CLOSE));
m_ImgLstCloseButton.Add(AfxGetApp()->LoadIcon(IDI_CLOSE_2));
m_ImgLstCloseButton.GetImageInfo(0, &m_iiCloseButton);
Invalidate();
}
}
void CClosableTabCtrl::OnContextMenu(CWnd* pWnd, CPoint point)
{
/*
if (m_bCloseable)
{
if (point.x == -1 || point.y == -1) {
if (!SetDefaultContextMenuPos())
return;
point = m_ptCtxMenu;
ClientToScreen(&point);
}
else {
m_ptCtxMenu = point;
ScreenToClient(&m_ptCtxMenu);
}
int iTab = GetTabUnderPoint(m_ptCtxMenu);
if (iTab != -1)
{
if (GetParent()->SendMessage(UM_QUERYTAB, (WPARAM)iTab) == 0)
{
CMenu menu;
menu.CreatePopupMenu();
menu.AppendMenu(MF_STRING, MP_REMOVE, GetResString(IDS_FD_CLOSE));
menu.TrackPopupMenu(TPM_LEFTALIGN | TPM_RIGHTBUTTON, point.x, point.y, this);
}
}
}
*/
return CTabCtrl::OnContextMenu(pWnd, point);
}
BOOL CClosableTabCtrl::OnCommand(WPARAM wParam, LPARAM lParam)
{
/*
if (wParam == MP_REMOVE)
{
if (m_ptCtxMenu.x != -1 && m_ptCtxMenu.y != -1)
{
int iTab = GetTabUnderPoint(m_ptCtxMenu);
if (iTab != -1) {
GetParent()->SendMessage(UM_CLOSETAB, (WPARAM)iTab);
return TRUE;
}
}
}
*/
return CTabCtrl::OnCommand(wParam, lParam);
}
LRESULT CClosableTabCtrl::_OnThemeChanged()
{
// Owner drawn tab control seems to have troubles with updating itself due to an XP theme change..
ModifyStyle(TCS_OWNERDRAWFIXED, 0); // Reset control style to not-owner drawn
Default(); // Process original WM_THEMECHANGED message
ModifyStyle(0, TCS_OWNERDRAWFIXED); // Apply owner drawn style again
return 0;
}
// Vista: This gets never called for an owner drawn tab control
HBRUSH CClosableTabCtrl::CtlColor(CDC* /*pDC*/, UINT /*nCtlColor*/)
{
// Change any attributes of the DC here
// Return a non-NULL brush if the parent's handler should not be called
return NULL;
}
// Vista: This gets never called for an owner drawn tab control
HBRUSH CClosableTabCtrl::OnCtlColor(CDC* pDC, CWnd* pWnd, UINT nCtlColor)
{
HBRUSH hbr = CTabCtrl::OnCtlColor(pDC, pWnd, nCtlColor);
// Change any attributes of the DC here
// Return a different brush if the default is not desired
return hbr;
}
// Vista: Can not be used to workaround the problems with owner drawn tab control
BOOL CClosableTabCtrl::OnEraseBkgnd(CDC* pDC)
{
return CTabCtrl::OnEraseBkgnd(pDC);
}
BOOL CClosableTabCtrl::DeleteItem(int nItem)
{
// if we remove a tab which would lead to scrolling back to other tabs, all those become hidden for... whatever reasons
// its easy enough wo work arround by scrolling to the first visible tab _before_ we delete the other one
SetCurSel(0);
return __super::DeleteItem(nItem);
}
bool CClosableTabCtrl::IsVistaThemeActive() const
{
return g_xpStyle.IsThemeActive() && g_xpStyle.IsAppThemed();
}