comctl32/button: Implement split buttons.

Wine-Bug: https://bugs.winehq.org/show_bug.cgi?id=20123
Signed-off-by: Gabriel Ivăncescu <gabrielopcode@gmail.com>
Signed-off-by: Nikolay Sivov <nsivov@codeweavers.com>
Signed-off-by: Alexandre Julliard <julliard@winehq.org>
This commit is contained in:
Gabriel Ivăncescu 2019-04-03 20:06:29 +03:00 committed by Alexandre Julliard
parent ae4d8e1838
commit ee2f71d8f6
1 changed files with 259 additions and 3 deletions

View File

@ -104,7 +104,11 @@ static void CB_Paint( const BUTTON_INFO *infoPtr, HDC hDC, UINT action );
static void GB_Paint( const BUTTON_INFO *infoPtr, HDC hDC, UINT action );
static void UB_Paint( const BUTTON_INFO *infoPtr, HDC hDC, UINT action );
static void OB_Paint( const BUTTON_INFO *infoPtr, HDC hDC, UINT action );
static void SB_Paint( const BUTTON_INFO *infoPtr, HDC hDC, UINT action );
static void BUTTON_CheckAutoRadioButton( HWND hwnd );
static void get_split_button_rects(const BUTTON_INFO*, const RECT*, RECT*, RECT*);
static BOOL notify_split_button_dropdown(const BUTTON_INFO*, const POINT*, HWND);
static void draw_split_button_dropdown_glyph(const BUTTON_INFO*, HDC, RECT*);
#define MAX_BTN_TYPE 16
@ -155,8 +159,8 @@ static const pfPaint btnPaintFunc[MAX_BTN_TYPE] =
CB_Paint, /* BS_AUTORADIOBUTTON */
NULL, /* BS_PUSHBOX */
OB_Paint, /* BS_OWNERDRAW */
PB_Paint, /* BS_SPLITBUTTON */
PB_Paint, /* BS_DEFSPLITBUTTON */
SB_Paint, /* BS_SPLITBUTTON */
SB_Paint, /* BS_DEFSPLITBUTTON */
PB_Paint, /* BS_COMMANDLINK */
PB_Paint /* BS_DEFCOMMANDLINK */
};
@ -554,6 +558,11 @@ static LRESULT CALLBACK BUTTON_WindowProc(HWND hWnd, UINT uMsg, WPARAM wParam, L
infoPtr->state |= BUTTON_BTNPRESSED;
SetCapture( hWnd );
}
else if (wParam == VK_UP || wParam == VK_DOWN)
{
/* Up and down arrows work on every button, and even with BCSS_NOSPLIT */
notify_split_button_dropdown(infoPtr, NULL, hWnd);
}
break;
case WM_LBUTTONDBLCLK:
@ -567,8 +576,14 @@ static LRESULT CALLBACK BUTTON_WindowProc(HWND hWnd, UINT uMsg, WPARAM wParam, L
}
/* fall through */
case WM_LBUTTONDOWN:
SetCapture( hWnd );
SetFocus( hWnd );
if ((btn_type == BS_SPLITBUTTON || btn_type == BS_DEFSPLITBUTTON) &&
!(infoPtr->split_style & BCSS_NOSPLIT) &&
notify_split_button_dropdown(infoPtr, &pt, hWnd))
break;
SetCapture( hWnd );
infoPtr->state |= BUTTON_BTNPRESSED;
SendMessageW( hWnd, BM_SETSTATE, TRUE, 0 );
break;
@ -579,6 +594,8 @@ static LRESULT CALLBACK BUTTON_WindowProc(HWND hWnd, UINT uMsg, WPARAM wParam, L
/* fall through */
case WM_LBUTTONUP:
state = infoPtr->state;
if (state & BST_DROPDOWNPUSHED)
SendMessageW(hWnd, BCM_SETDROPDOWNSTATE, FALSE, 0);
if (!(state & BUTTON_BTNPRESSED)) break;
infoPtr->state &= BUTTON_NSTATES;
if (!(state & BST_PUSHED))
@ -2044,6 +2061,245 @@ static void OB_Paint( const BUTTON_INFO *infoPtr, HDC hDC, UINT action )
if (hrgn) DeleteObject( hrgn );
}
/**********************************************************************
* Split Button Functions
*/
static void SB_Paint( const BUTTON_INFO *infoPtr, HDC hDC, UINT action )
{
LONG style = GetWindowLongW(infoPtr->hwnd, GWL_STYLE);
LONG state = infoPtr->state;
UINT dtFlags = (UINT)-1L;
RECT rc, push_rect, dropdown_rect;
NMCUSTOMDRAW nmcd;
HPEN pen, old_pen;
HBRUSH old_brush;
INT old_bk_mode;
LRESULT cdrf;
HWND parent;
HRGN hrgn;
GetClientRect(infoPtr->hwnd, &rc);
/* Send WM_CTLCOLOR to allow changing the font (the colors are fixed) */
if (infoPtr->font) SelectObject(hDC, infoPtr->font);
if (!(parent = GetParent(infoPtr->hwnd))) parent = infoPtr->hwnd;
SendMessageW(parent, WM_CTLCOLORBTN, (WPARAM)hDC, (LPARAM)infoPtr->hwnd);
hrgn = set_control_clipping(hDC, &rc);
pen = CreatePen(PS_SOLID, 1, GetSysColor(COLOR_WINDOWFRAME));
old_pen = SelectObject(hDC, pen);
old_brush = SelectObject(hDC, GetSysColorBrush(COLOR_BTNFACE));
old_bk_mode = SetBkMode(hDC, TRANSPARENT);
init_custom_draw(&nmcd, infoPtr, hDC, &rc);
/* Send erase notifications */
cdrf = SendMessageW(parent, WM_NOTIFY, nmcd.hdr.idFrom, (LPARAM)&nmcd);
if (cdrf & CDRF_SKIPDEFAULT) goto cleanup;
if (get_button_type(style) == BS_DEFSPLITBUTTON)
{
if (action != ODA_FOCUS)
Rectangle(hDC, rc.left, rc.top, rc.right, rc.bottom);
InflateRect(&rc, -1, -1);
/* The split will now be off by 1 pixel, but
that's exactly what Windows does as well */
}
get_split_button_rects(infoPtr, &rc, &push_rect, &dropdown_rect);
if (infoPtr->split_style & BCSS_NOSPLIT)
push_rect = rc;
/* Skip the frame drawing if only focus has changed */
if (action != ODA_FOCUS)
{
UINT flags = DFCS_BUTTONPUSH;
if (style & BS_FLAT) flags |= DFCS_MONO;
else if (state & BST_PUSHED)
flags |= (get_button_type(style) == BS_DEFSPLITBUTTON)
? DFCS_FLAT : DFCS_PUSHED;
if (state & (BST_CHECKED | BST_INDETERMINATE))
flags |= DFCS_CHECKED;
if (infoPtr->split_style & BCSS_NOSPLIT)
DrawFrameControl(hDC, &push_rect, DFC_BUTTON, flags);
else
{
UINT dropdown_flags = flags & ~DFCS_CHECKED;
if (state & BST_DROPDOWNPUSHED)
dropdown_flags = (dropdown_flags & ~DFCS_FLAT) | DFCS_PUSHED;
/* Adjust for shadow and draw order so it looks properly */
if (infoPtr->split_style & BCSS_ALIGNLEFT)
{
dropdown_rect.right++;
DrawFrameControl(hDC, &dropdown_rect, DFC_BUTTON, dropdown_flags);
dropdown_rect.right--;
DrawFrameControl(hDC, &push_rect, DFC_BUTTON, flags);
}
else
{
push_rect.right++;
DrawFrameControl(hDC, &push_rect, DFC_BUTTON, flags);
push_rect.right--;
DrawFrameControl(hDC, &dropdown_rect, DFC_BUTTON, dropdown_flags);
}
}
}
if (cdrf & CDRF_NOTIFYPOSTERASE)
{
nmcd.dwDrawStage = CDDS_POSTERASE;
SendMessageW(parent, WM_NOTIFY, nmcd.hdr.idFrom, (LPARAM)&nmcd);
}
/* Send paint notifications */
nmcd.dwDrawStage = CDDS_PREPAINT;
cdrf = SendMessageW(parent, WM_NOTIFY, nmcd.hdr.idFrom, (LPARAM)&nmcd);
if (cdrf & CDRF_SKIPDEFAULT) goto cleanup;
/* Shrink push button rect so that the content won't touch the surrounding frame */
InflateRect(&push_rect, -2, -2);
if (!(cdrf & CDRF_DOERASE) && action != ODA_FOCUS)
{
COLORREF old_color = SetTextColor(hDC, GetSysColor(COLOR_BTNTEXT));
RECT label_rect = push_rect, image_rect, text_rect;
dtFlags = BUTTON_CalcLayoutRects(infoPtr, hDC, &label_rect, &image_rect, &text_rect);
if (dtFlags != (UINT)-1L)
BUTTON_DrawLabel(infoPtr, hDC, dtFlags, &image_rect, &text_rect);
draw_split_button_dropdown_glyph(infoPtr, hDC, &dropdown_rect);
SetTextColor(hDC, old_color);
}
if (cdrf & CDRF_NOTIFYPOSTPAINT)
{
nmcd.dwDrawStage = CDDS_POSTPAINT;
SendMessageW(parent, WM_NOTIFY, nmcd.hdr.idFrom, (LPARAM)&nmcd);
}
if ((cdrf & CDRF_SKIPPOSTPAINT) || dtFlags == (UINT)-1L) goto cleanup;
if (action == ODA_FOCUS || (state & BST_FOCUS))
DrawFocusRect(hDC, &push_rect);
cleanup:
SelectObject(hDC, old_pen);
SelectObject(hDC, old_brush);
SetBkMode(hDC, old_bk_mode);
SelectClipRgn(hDC, hrgn);
if (hrgn) DeleteObject(hrgn);
DeleteObject(pen);
}
/* Given the full button rect of the split button, retrieve the push part and the dropdown part */
static inline void get_split_button_rects(const BUTTON_INFO *infoPtr, const RECT *button_rect,
RECT *push_rect, RECT *dropdown_rect)
{
*push_rect = *dropdown_rect = *button_rect;
/* The dropdown takes priority if the client rect is too small, it will only have a dropdown */
if (infoPtr->split_style & BCSS_ALIGNLEFT)
{
dropdown_rect->right = min(button_rect->left + infoPtr->glyph_size.cx, button_rect->right);
push_rect->left = dropdown_rect->right;
}
else
{
dropdown_rect->left = max(button_rect->right - infoPtr->glyph_size.cx, button_rect->left);
push_rect->right = dropdown_rect->left;
}
}
/* Notify the parent if the point is within the dropdown and return TRUE (always notify if NULL) */
static BOOL notify_split_button_dropdown(const BUTTON_INFO *infoPtr, const POINT *pt, HWND hwnd)
{
NMBCDROPDOWN nmbcd;
GetClientRect(hwnd, &nmbcd.rcButton);
if (pt)
{
RECT push_rect, dropdown_rect;
get_split_button_rects(infoPtr, &nmbcd.rcButton, &push_rect, &dropdown_rect);
if (!PtInRect(&dropdown_rect, *pt))
return FALSE;
/* If it's already down (set manually via BCM_SETDROPDOWNSTATE), fake the notify */
if (infoPtr->state & BST_DROPDOWNPUSHED)
return TRUE;
}
SendMessageW(hwnd, BCM_SETDROPDOWNSTATE, TRUE, 0);
nmbcd.hdr.hwndFrom = hwnd;
nmbcd.hdr.idFrom = GetWindowLongPtrW(hwnd, GWLP_ID);
nmbcd.hdr.code = BCN_DROPDOWN;
SendMessageW(GetParent(hwnd), WM_NOTIFY, nmbcd.hdr.idFrom, (LPARAM)&nmbcd);
SendMessageW(hwnd, BCM_SETDROPDOWNSTATE, FALSE, 0);
return TRUE;
}
/* Draw the split button dropdown glyph or image */
static void draw_split_button_dropdown_glyph(const BUTTON_INFO *infoPtr, HDC hdc, RECT *rect)
{
if (infoPtr->split_style & BCSS_IMAGE)
{
int w, h;
/* When the glyph is an image list, Windows is very buggy with BCSS_STRETCH,
positions it weirdly and doesn't even stretch it, but instead extends the
image, leaking into other images in the list (or black if none). Instead,
we'll ignore this and just position it at center as without BCSS_STRETCH. */
if (!ImageList_GetIconSize(infoPtr->glyph, &w, &h)) return;
ImageList_Draw(infoPtr->glyph,
(ImageList_GetImageCount(infoPtr->glyph) == 1) ? 0 : get_draw_state(infoPtr) - 1,
hdc, rect->left + (rect->right - rect->left - w) / 2,
rect->top + (rect->bottom - rect->top - h) / 2, ILD_NORMAL);
}
else if (infoPtr->glyph_size.cy >= 0)
{
/* infoPtr->glyph is a character code from Marlett */
HFONT font, old_font;
LOGFONTW logfont = { 0, 0, 0, 0, FW_NORMAL, 0, 0, 0, SYMBOL_CHARSET, 0, 0, 0, 0,
{ 'M','a','r','l','e','t','t',0 } };
if (infoPtr->glyph_size.cy)
{
/* BCSS_STRETCH preserves aspect ratio, uses minimum as size */
if (infoPtr->split_style & BCSS_STRETCH)
logfont.lfHeight = min(infoPtr->glyph_size.cx, infoPtr->glyph_size.cy);
else
{
logfont.lfWidth = infoPtr->glyph_size.cx;
logfont.lfHeight = infoPtr->glyph_size.cy;
}
}
else logfont.lfHeight = infoPtr->glyph_size.cx;
if ((font = CreateFontIndirectW(&logfont)))
{
old_font = SelectObject(hdc, font);
DrawTextW(hdc, (const WCHAR*)&infoPtr->glyph, 1, rect,
DT_CENTER | DT_VCENTER | DT_SINGLELINE | DT_NOCLIP | DT_NOPREFIX);
SelectObject(hdc, old_font);
DeleteObject(font);
}
}
}
/**********************************************************************
* Themed Paint Functions
*/
static void PB_ThemedPaint(HTHEME theme, const BUTTON_INFO *infoPtr, HDC hDC, int state, UINT dtFlags, BOOL focused)
{
RECT bgRect, textRect;