#include "alfpriv.h" #include "alfcompat.h" #define ALF_NB_ADDTAB (ALF_WM__BASE + 300) #define ALF_NB_TABCOUNT (ALF_WM__BASE + 301) #define ALF_NB_GETPANEL (ALF_WM__BASE + 302) #define ALF_NB_GETSELINDEX (ALF_WM__BASE + 303) #define ALF_NB_GETPNLINDEX (ALF_WM__BASE + 304) #define TABP_BODY 10 #define TMT_FILLCOLORHINT 3821 typedef struct { HWND hwndTabCtrl; HTHEME hTheme; ALFColor tabPaneBgColor; DWORD flags; int dpi; int prevSelectedIndex; } ALFNotebookPriv; typedef struct { TCITEMHEADER header; DWORD layoutFlags; HWND panel; } ALFNotebookPage; static ALFNotebookPriv * ALF_Notebook_CreatePriv(void) { ALFNotebookPriv *priv = ALF_New(ALFNotebookPriv, 1); priv->tabPaneBgColor = ALF_COLOR_SYS(COLOR_BTNFACE); priv->prevSelectedIndex = -1; return priv; } void ALF_Notebook_FreePriv(ALFNotebookPriv *priv) { ALF_Compat_CloseThemeData(priv->hTheme); priv->hTheme = NULL; ALF_Free(priv); } static int ALF_Notebook_InternalTabCount(HWND notebook, HWND tabControl) { (void)notebook; return TabCtrl_GetItemCount(tabControl); } static HWND ALF_Notebook_InternalTabPanel(HWND notebook, HWND tabControl, int index) { (void)notebook; ALFNotebookPage p; ZeroMemory(&p, sizeof(p)); p.header.mask = TCIF_PARAM; if (TabCtrl_GetItem(tabControl, index, &p)) { return p.panel; } else { return NULL; } } static int ALF_Notebook_InternalSelectedIndex(HWND notebook, HWND tabControl) { (void)notebook; return TabCtrl_GetCurSel(tabControl); } static void ALF_Notebook_InternalHandleTabChange(HWND hwndNotebook, ALFNotebookPriv *priv) { int selectedIndex = ALF_Notebook_InternalSelectedIndex(hwndNotebook, priv->hwndTabCtrl); int n = ALF_Notebook_InternalTabCount(hwndNotebook, priv->hwndTabCtrl); for (int i = 0; i < n; ++i) { if (i == selectedIndex) { SetWindowPos(ALF_Notebook_InternalTabPanel(hwndNotebook, priv->hwndTabCtrl, i), HWND_TOP, 0, 0, 0, 0, SWP_NOACTIVATE | SWP_NOSIZE | SWP_NOMOVE | SWP_SHOWWINDOW); } else { ShowWindow(ALF_Notebook_InternalTabPanel(hwndNotebook, priv->hwndTabCtrl, i), SW_HIDE); } } if (selectedIndex != priv->prevSelectedIndex) { HWND currentFocus = GetFocus(); if (currentFocus != priv->hwndTabCtrl && IsChild(hwndNotebook, currentFocus)) { HWND focusControl = GetNextDlgTabItem(hwndNotebook, priv->hwndTabCtrl, FALSE); ALF_SetFocus(focusControl); } } priv->prevSelectedIndex = selectedIndex; } static void ALF_Notebook_SetSingleTabBackground(HWND hwndNotebook, ALFNotebookPriv *priv, HWND panel) { (void)hwndNotebook; SendMessage(panel, ALF_WM_SETBGCOLOR, 0, (LPARAM)priv->tabPaneBgColor); } static void ALF_Notebook_SetAllTabBackgrounds(HWND hwndNotebook, ALFNotebookPriv *priv) { int n = ALF_Notebook_InternalTabCount(hwndNotebook, priv->hwndTabCtrl); for (int i = 0; i < n; ++i) { ALFNotebookPage p; ZeroMemory(&p, sizeof(p)); p.header.mask = TCIF_PARAM; if (TabCtrl_GetItem(priv->hwndTabCtrl, i, &p)) { if (p.layoutFlags & ALF_LAYOUT_INHERITBGCOLOR) { ALF_Notebook_SetSingleTabBackground(hwndNotebook, priv, p.panel); } } } } static void ALF_Notebook_PropagateFontToPages(HWND hwndNotebook, ALFNotebookPriv *priv, HFONT font, LPARAM lparam) { int n = ALF_Notebook_InternalTabCount(hwndNotebook, priv->hwndTabCtrl); for (int i = 0; i < n; ++i) { ALFNotebookPage p; ZeroMemory(&p, sizeof(p)); p.header.mask = TCIF_PARAM; if (TabCtrl_GetItem(priv->hwndTabCtrl, i, &p)) { if (p.layoutFlags & ALF_LAYOUT_INHERITFONT) { SendMessage(p.panel, WM_SETFONT, (WPARAM)font, lparam); } } } } static void ALF_Notebook_PropagateDpiChange(HWND hwndNotebook, ALFNotebookPriv *priv, WPARAM wparam, LPARAM lparam) { int n = ALF_Notebook_InternalTabCount(hwndNotebook, priv->hwndTabCtrl); for (int i = 0; i < n; ++i) { HWND hwndPanel = ALF_NotebookTabPanel(hwndNotebook, i); SendMessage(hwndPanel, ALF_WM_DPICHANGE, wparam, lparam); } } static BOOL ALF_Notebook_CheckSingleColorBits(void *bits, SIZE s, COLORREF *outColor) { int l = s.cx * s.cy; DWORD c; CopyMemory(&c, bits, 4); c &= 0x00FFFFFF; for (int i = 1; i < l; ++i) { DWORD c2; CopyMemory(&c2, (char*)bits + i*4, 4); c2 &= 0x00FFFFFF; if (c2 != c) return FALSE; } *outColor = c; return TRUE; } static void ALF_Notebook_InternalHandleThemeChange(HWND hwndNotebook, ALFNotebookPriv *priv) { ALF_Compat_CloseThemeData(priv->hTheme); priv->hTheme = NULL; priv->tabPaneBgColor = ALF_COLOR_SYS(COLOR_BTNFACE); if (ALF_Compat_IsAppThemed()) priv->hTheme = ALF_Compat_OpenThemeData(hwndNotebook, L"Tab"); InvalidateRect(hwndNotebook, NULL, TRUE); ALF_InvalidateLayout(hwndNotebook); if (priv->hTheme) { if (priv->flags & ALF_NOTEBOOK_SOLID_TAB_BACKGROUND) { COLORREF c; if (SUCCEEDED(ALF_Compat_GetThemeColor(priv->hTheme, TABP_BODY, 0, TMT_FILLCOLORHINT, &c))) { priv->tabPaneBgColor = (ALFColor)c; } } else { priv->tabPaneBgColor = ALF_COLOR_TRANSPARENT; // SPEED HACK: check if tab pane background is a single solid color // Windows 10 really sucks at stretching a completely white bitmap HDC dcNotebook = GetDC(hwndNotebook); HDC dcMem = CreateCompatibleDC(dcNotebook); SIZE s; if (SUCCEEDED(ALF_Compat_GetThemePartSize(priv->hTheme, dcNotebook, TABP_BODY, 0, NULL, TS_TRUE, &s))) { BITMAPINFO bi; ZeroMemory(&bi, sizeof(bi)); bi.bmiHeader.biSize = sizeof(bi.bmiHeader); bi.bmiHeader.biWidth = s.cx; bi.bmiHeader.biHeight = s.cy; bi.bmiHeader.biPlanes = 1; bi.bmiHeader.biBitCount = 32; bi.bmiHeader.biCompression = BI_RGB; void *bits = NULL; HBITMAP hDib = CreateDIBSection(dcMem, &bi, DIB_RGB_COLORS, &bits, NULL, 0); if (hDib) { HBITMAP hbmOld = SelectBitmap(dcMem, hDib); RECT r = { 0, 0, s.cx, s.cy }; if (SUCCEEDED(ALF_Compat_DrawThemeBackground(priv->hTheme, dcMem, TABP_BODY, 0, &r, NULL))) { GdiFlush(); COLORREF c = 0; if (ALF_Notebook_CheckSingleColorBits(bits, s, &c)) { priv->tabPaneBgColor = (ALFColor)c; } } SelectBitmap(dcMem, hbmOld); DeleteObject(hDib); } } DeleteDC(dcMem); ReleaseDC(hwndNotebook, dcNotebook); } } ALF_Notebook_SetAllTabBackgrounds(hwndNotebook, priv); } static HWND ALF_Notebook_InternalAddTab(HWND notebook, ALFNotebookPriv *priv, const TCHAR *title) { RECT r; GetClientRect(priv->hwndTabCtrl, &r); TabCtrl_AdjustRect(priv->hwndTabCtrl, FALSE, &r); MapWindowRect(priv->hwndTabCtrl, notebook, &r); HWND hwndPanel = ALF_CreatePanelWindow(notebook, (WORD)-1); SetWindowPos(hwndPanel, NULL, r.left, r.top, r.right - r.left, r.bottom - r.top, SWP_HIDEWINDOW|SWP_NOACTIVATE|SWP_NOZORDER|SWP_NOOWNERZORDER); SendMessage(hwndPanel, WM_SETFONT, (WPARAM)SendMessage(priv->hwndTabCtrl, WM_GETFONT, 0, 0), 0); SendMessage(hwndPanel, ALF_WM_DPICHANGE, 0, (LPARAM)priv->dpi); ALF_Notebook_SetSingleTabBackground(notebook, priv, hwndPanel); ALFNotebookPage p; ZeroMemory(&p, sizeof(p)); p.header.mask = TCIF_TEXT | TCIF_PARAM; p.header.pszText = (TCHAR*)title; p.layoutFlags = ALF_LAYOUT_INHERITBGCOLOR | ALF_LAYOUT_INHERITFONT; p.panel = hwndPanel; TabCtrl_InsertItem(priv->hwndTabCtrl, ALF_Notebook_InternalTabCount(notebook, priv->hwndTabCtrl), &p); ALF_Notebook_InternalHandleTabChange(notebook, priv); ALF_InvalidateLayout(notebook); return hwndPanel; } static LRESULT CALLBACK ALF_Notebook_TabCtrlWndProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam) { WNDPROC oldWndProc = (WNDPROC)GetWindowLongPtr(hwnd, GWLP_USERDATA); if (uMsg == WM_ERASEBKGND) { // see: http://www.virtualdub.org/blog/pivot/entry.php?id=291 HDC hdc = (HDC)wParam; RECT r; GetClientRect(hwnd, &r); TabCtrl_AdjustRect(hwnd, FALSE, &r); ExcludeClipRect(hdc, r.left, r.top, r.right, r.bottom); } if (uMsg == WM_DESTROY) { SetWindowLongPtr(hwnd, GWLP_WNDPROC, (LONG_PTR)oldWndProc); SetWindowLongPtr(hwnd, GWLP_USERDATA, 0); } return CallWindowProc(oldWndProc, hwnd, uMsg, wParam, lParam); } static void ALF_Notebook_InternalPaint(HWND hwnd, ALFNotebookPriv *priv, HDC dc, RECT *f) { if (priv->hTheme != NULL) { RECT r; GetClientRect(priv->hwndTabCtrl, &r); TabCtrl_AdjustRect(priv->hwndTabCtrl, FALSE, &r); MapWindowRect(priv->hwndTabCtrl, hwnd, &r); SIZE s; if (SUCCEEDED(ALF_Compat_GetThemePartSize(priv->hTheme, dc, TABP_BODY, 0, NULL, TS_TRUE, &s))) { // windows dialog tab panes tile the background horizontally for (int x = r.left; x < r.right; x += s.cx) { RECT r2 = { x, r.top, x + s.cx, r.bottom }; if (r2.right > r.right) r2.right = r.right; if (f && r2.left > f->right && r2.right < f->left) continue; if (RectVisible(dc, &r2)) ALF_Compat_DrawThemeBackground(priv->hTheme, dc, TABP_BODY, 0, &r2, NULL); } } else { // old version - let uxtheme stretch it ALF_Compat_DrawThemeBackground(priv->hTheme, dc, TABP_BODY, 0, &r, NULL); } ExcludeClipRect(dc, r.left, r.top, r.right, r.bottom); } FillRect(dc, f, GetSysColorBrush(COLOR_BTNFACE)); } static DWORD ALF_Notebook_GetWidgetFlags(HWND hwndNotebook, ALFNotebookPriv *priv, HWND widget) { int n = ALF_Notebook_InternalTabCount(hwndNotebook, priv->hwndTabCtrl); for (int i = 0; i < n; ++i) { ALFNotebookPage p; ZeroMemory(&p, sizeof(p)); p.header.mask = TCIF_PARAM; if (TabCtrl_GetItem(priv->hwndTabCtrl, i, &p)) { if (p.panel == widget) { return p.layoutFlags; } } } return 0; } static BOOL ALF_Notebook_SetWidgetFlags(HWND hwndNotebook, ALFNotebookPriv *priv, HWND widget, DWORD flags) { int n = ALF_Notebook_InternalTabCount(hwndNotebook, priv->hwndTabCtrl); for (int i = 0; i < n; ++i) { ALFNotebookPage p; ZeroMemory(&p, sizeof(p)); p.header.mask = TCIF_PARAM; if (TabCtrl_GetItem(priv->hwndTabCtrl, i, &p)) { if (p.panel == widget) { if (p.layoutFlags == flags) return TRUE; if ((flags & ALF_LAYOUT_INHERITFONT) && !(p.layoutFlags & ALF_LAYOUT_INHERITFONT)) { SendMessage(p.panel, WM_SETFONT, (WPARAM)SendMessage(priv->hwndTabCtrl, WM_GETFONT, 0, 0), TRUE); } if ((flags & ALF_LAYOUT_INHERITBGCOLOR) && !(p.layoutFlags & ALF_LAYOUT_INHERITBGCOLOR)) { ALF_Notebook_SetSingleTabBackground(hwndNotebook, priv, p.panel); } p.layoutFlags = flags; return TabCtrl_SetItem(priv->hwndTabCtrl, i, &p); } } } return FALSE; } static LRESULT CALLBACK ALF__NotebookWindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam) { ALFNotebookPriv *priv = (ALFNotebookPriv *)GetWindowLongPtr(hwnd, 0); if (uMsg == WM_NOTIFY) { NMHDR *pnmh = (NMHDR*)lParam; if (pnmh->hwndFrom == priv->hwndTabCtrl && pnmh->code == TCN_SELCHANGE) { ALF_Notebook_InternalHandleTabChange(hwnd, priv); return TRUE; } } if (ALF_ShouldMessageBubble(hwnd, uMsg, wParam, lParam)) return SendMessage(GetParent(hwnd), uMsg, wParam, lParam); if (uMsg == WM_CREATE && priv == NULL) { priv = ALF_Notebook_CreatePriv(); priv->hwndTabCtrl = CreateWindowEx(0, WC_TABCONTROL, TEXT(""), WS_CHILD | WS_VISIBLE | WS_TABSTOP | WS_CLIPSIBLINGS, 0, 0, 100, 100, hwnd, NULL, ALF_HINSTANCE, NULL); TabCtrl_SetItemExtra(priv->hwndTabCtrl, sizeof(ALFNotebookPage) - sizeof(TCITEMHEADER)); LONG_PTR wndProc = GetWindowLongPtr(priv->hwndTabCtrl, GWLP_WNDPROC); SetWindowLongPtr(priv->hwndTabCtrl, GWLP_USERDATA, wndProc); SetWindowLongPtr(priv->hwndTabCtrl, GWLP_WNDPROC, (LONG_PTR)ALF_Notebook_TabCtrlWndProc); SetWindowLongPtr(hwnd, 0, (LONG_PTR)priv); ALF_Notebook_InternalHandleThemeChange(hwnd, priv); } if (!priv) return DefWindowProc(hwnd, uMsg, wParam, lParam); if (uMsg == WM_DESTROY) { ALF_Notebook_FreePriv(priv); SetWindowLongPtr(hwnd, 0, 0); priv = NULL; } else if (uMsg == WM_SETFONT) { SendMessage(priv->hwndTabCtrl, WM_SETFONT, wParam, lParam); ALF_Notebook_PropagateFontToPages(hwnd, priv, (HFONT)wParam, lParam); ALF_InvalidateLayout(hwnd); } else if (uMsg == WM_GETFONT) { return SendMessage(priv->hwndTabCtrl, WM_GETFONT, wParam, lParam); } else if (uMsg == ALF_WM_QUERYSIZE) { int n = ALF_Notebook_InternalTabCount(hwnd, priv->hwndTabCtrl); RECT r = { 0, 0, 0, 0 }; for (int i = 0; i < n; ++i) { HWND p = ALF_Notebook_InternalTabPanel(hwnd, priv->hwndTabCtrl, i); SIZE s = { 0, 0 }; SendMessage(p, ALF_WM_QUERYSIZE, 0, (LPARAM)&s); if (s.cx > r.right) r.right = s.cx; if (s.cy > r.bottom) r.bottom = s.cy; } if (!r.right) r.right = 100; // FIXME! if (!r.bottom) r.bottom = 100; // FIXME! TabCtrl_AdjustRect(priv->hwndTabCtrl, TRUE, &r); SIZE *ps = (SIZE*)lParam; if (ps->cx < r.right - r.left) { ps->cx = r.right - r.left; } if (ps->cy < r.bottom - r.top) { ps->cy = r.bottom - r.top; } return 0; } else if (uMsg == WM_WINDOWPOSCHANGED) { WINDOWPOS *pos = (WINDOWPOS *)lParam; if (!(pos->flags & SWP_NOSIZE)) { RECT r; GetClientRect(hwnd, &r); int n = ALF_Notebook_InternalTabCount(hwnd, priv->hwndTabCtrl); RECT oldR; GetWindowRect(priv->hwndTabCtrl, &oldR); TabCtrl_AdjustRect(priv->hwndTabCtrl, FALSE, &oldR); HDWP hdwp = BeginDeferWindowPos(n+1); // SWP_COPYBITS: NT 3.51 sadness, will not invalidate bottom edge properly on resize // even though it redraws everything else with great flicker // the flicker problem is present in every other version of windows, too hdwp = DeferWindowPos(hdwp, priv->hwndTabCtrl, NULL, 0, 0, r.right-r.left, r.bottom-r.top, SWP_NOMOVE|SWP_NOACTIVATE|SWP_NOZORDER|SWP_NOCOPYBITS); TabCtrl_AdjustRect(priv->hwndTabCtrl, FALSE, &r); for (int i = 0; i < n; ++i) { HWND p = ALF_Notebook_InternalTabPanel(hwnd, priv->hwndTabCtrl, i); hdwp = DeferWindowPos(hdwp, p, NULL, r.left, r.top, r.right - r.left, r.bottom - r.top, SWP_NOACTIVATE|SWP_NOZORDER); } EndDeferWindowPos(hdwp); if (priv->hTheme && priv->tabPaneBgColor == ALF_COLOR_TRANSPARENT && ((oldR.bottom - oldR.top) != (r.bottom - r.top))) { // only needed when height changes, since bg is tiled horizontally HWND panel = ALF_NotebookSelectedPanel(hwnd); if (panel != NULL) ALF_InvalidateBackground(panel); } } } else if (uMsg == ALF_NB_ADDTAB) { return (LRESULT)ALF_Notebook_InternalAddTab(hwnd, priv, (TCHAR *)lParam); } else if (uMsg == ALF_NB_TABCOUNT) { return (LRESULT)ALF_Notebook_InternalTabCount(hwnd, priv->hwndTabCtrl); } else if (uMsg == ALF_NB_GETPANEL) { return (LRESULT)ALF_Notebook_InternalTabPanel(hwnd, priv->hwndTabCtrl, (int)lParam); } else if (uMsg == ALF_NB_GETSELINDEX) { return (LRESULT)ALF_Notebook_InternalSelectedIndex(hwnd, priv->hwndTabCtrl); } else if (uMsg == WM_THEMECHANGED) { ALF_Notebook_InternalHandleThemeChange(hwnd, priv); } else if (uMsg == WM_ERASEBKGND) { return TRUE; } else if (uMsg == WM_PRINTCLIENT) { RECT r = {0, 0, 0, 0}; GetClientRect(hwnd, &r); ALF_Notebook_InternalPaint(hwnd, priv, (HDC)wParam, &r); return TRUE; } else if (uMsg == WM_PAINT) { PAINTSTRUCT ps; HDC dc = BeginPaint(hwnd, &ps); ALF_Notebook_InternalPaint(hwnd, priv, dc, &ps.rcPaint); EndPaint(hwnd, &ps); return 0; } else if (uMsg == ALF_WM_INVALIDATELAYOUT) { ALF_InvalidateLayout(GetParent(hwnd)); } else if (uMsg == ALF_WM_NTBK_GETFLAGS) { return (LRESULT)priv->flags; } else if (uMsg == ALF_WM_NTBK_SETFLAGS) { DWORD oldFlags = priv->flags; priv->flags = (DWORD)lParam; if ((oldFlags & ALF_NOTEBOOK_SOLID_TAB_BACKGROUND) != (priv->flags & ALF_NOTEBOOK_SOLID_TAB_BACKGROUND)) { ALF_Notebook_InternalHandleThemeChange(hwnd, priv); } } else if (uMsg == ALF_WM_LYT_GETWDGTFLAGS) { return (LRESULT)ALF_Notebook_GetWidgetFlags(hwnd, priv, (HWND)wParam); } else if (uMsg == ALF_WM_LYT_SETWDGTFLAGS) { return (LRESULT)ALF_Notebook_SetWidgetFlags(hwnd, priv, (HWND)wParam, (DWORD)lParam); } else if (uMsg == ALF_WM_DPICHANGE) { priv->dpi = (int)lParam; ALF_Notebook_PropagateDpiChange(hwnd, priv, wParam, lParam); return TRUE; } return DefWindowProc(hwnd, uMsg, wParam, lParam); } // tab control HWND ALF_AddNotebook(HWND parent, WORD id, int x, int y) { HWND hwndNtbk = ALF_CreateControlWindow(WS_EX_CONTROLPARENT, TEXT(""), WS_CHILD | WS_VISIBLE | WS_CLIPCHILDREN, 0, 0, 100, 100, parent, (HMENU)(ULONG_PTR)id, ALF__NotebookWindowProc, NULL); ALF_AddWidget(parent, x, y, hwndNtbk, 0, 0, ALF_LAYOUT_SIZE_QUERY | ALF_LAYOUT_INHERITFONT | ALF_LAYOUT_SENDDPICHANGE); return hwndNtbk; } HWND ALF_NotebookAddTab(HWND notebook, const TCHAR *title) { return (HWND)SendMessage(notebook, ALF_NB_ADDTAB, 0, (LPARAM)title); } int ALF_NotebookTabCount(HWND notebook) { return (int)SendMessage(notebook, ALF_NB_TABCOUNT, 0, 0); } int ALF_NotebookTabIndex(HWND notebook, HWND tabPanel); HWND ALF_NotebookTabPanel(HWND notebook, int index) { return (HWND)SendMessage(notebook, ALF_NB_GETPANEL, 0, (LPARAM)index); } int ALF_NotebookSelectedIndex(HWND notebook) { return (int)SendMessage(notebook, ALF_NB_GETSELINDEX, 0, 0); } HWND ALF_NotebookSelectedPanel(HWND notebook) { int i = ALF_NotebookSelectedIndex(notebook); if (i >= 0) { return ALF_NotebookTabPanel(notebook, i); } else { return NULL; } } DWORD ALF_NotebookFlags(HWND notebook) { return (DWORD)SendMessage(notebook, ALF_WM_NTBK_GETFLAGS, 0, 0); } void ALF_NotebookSetFlags(HWND notebook, DWORD flags) { SendMessage(notebook, ALF_WM_NTBK_SETFLAGS, 0, (LPARAM)flags); }