#include "cabinet.h" #include "trayclok.h" #include "tray.h" #include "util.h" #include "strsafe.h" class CClockCtl : public CImpWndProc { public: STDMETHODIMP_(ULONG) AddRef(); STDMETHODIMP_(ULONG) Release(); CClockCtl() : _cRef(1) {} protected: // Create & Destroy LRESULT _HandleCreate(); LRESULT _HandleDestroy(); // Paint helpers LRESULT _DoPaint(BOOL fPaint); void _EnsureFontsInitialized(BOOL fForce); void _GetTextExtent(HDC hdc, TCHAR* pszText, int cchText, LPRECT prcText); void _DrawText(HDC hdc, TCHAR* pszText, int cchText, LPRECT prcText); // Time/Date calc helpers void _Reset(); void _UpdateLastHour(); DWORD _RecalcCurTime(); void _EnableTimer(DWORD dtNextTick); // Message handlers void _HandleThemeChanged(WPARAM wParam); LRESULT _HandleIniChange(WPARAM wParam, LPTSTR pszSection); LRESULT _HandleTimeChange(); // Text extent helpers void _GetMaxTimeSize(HDC hdc, LPSIZE pszTime); void _GetMaxDateSize(HDC hdc, LPSIZE pszTime); void _GetMaxDaySize(HDC hdc, LPSIZE pszTime); LRESULT _CalcMinSize(int cxMax, int cyMax); // Tooltip text handler LRESULT _OnNeedText(LPTOOLTIPTEXT lpttt); // Window procedure LRESULT v_WndProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam); private: ULONG _cRef; int _cchCurDate; TCHAR _szDateFmt[40]; // The format string to pass to GetFormatTime TCHAR _szCurDate[40]; // The current Date string. int _cchCurTime; TCHAR _szTimeFmt[40]; // The format string to pass to GetFormatTime TCHAR _szCurTime[40]; // The current Time string. int _cchCurDay; TCHAR _szCurDay[40]; // The current Day string. WORD _wLastHour; // wHour from local time of last clock tick WORD _wLastMinute; // wMinute from local time of last clock tick HTHEME _hTheme; HFONT _hfontCapNormal; BOOL _fClockRunning; BOOL _fClockClipped; BOOL _fHasFocus; friend BOOL ClockCtl_Class(HINSTANCE hinst); }; ULONG CClockCtl::AddRef() { return ++_cRef; } ULONG CClockCtl::Release() { if (--_cRef == 0) { delete this; return 0; } return _cRef; } void CClockCtl::_UpdateLastHour() { SYSTEMTIME st; // Grab the time GetLocalTime(&st); _wLastHour = st.wHour; _wLastMinute = st.wMinute; } void CClockCtl::_EnableTimer(DWORD dtNextTick) { if (dtNextTick) { SetTimer(_hwnd, 0, dtNextTick, NULL); _fClockRunning = TRUE; } else if (_fClockRunning) { _fClockRunning = FALSE; KillTimer(_hwnd, 0); } } LRESULT CClockCtl::_HandleCreate() { AddRef(); _EnsureFontsInitialized(FALSE); _hTheme = OpenThemeData(_hwnd, L"Clock"); _UpdateLastHour(); return 1; } LRESULT CClockCtl::_HandleDestroy() { Release(); // safe because cwndproc is holding a ref across call to v_wndproc if (_hTheme) { CloseThemeData(_hTheme); _hTheme = NULL; } if (_hfontCapNormal) { DeleteFont(_hfontCapNormal); _hfontCapNormal = NULL; } _EnableTimer(0); return 1; } DWORD CClockCtl::_RecalcCurTime() { SYSTEMTIME st; // // Current time. // GetLocalTime(&st); // // Don't recalc the text if the time hasn't changed yet. // if ((st.wMinute != _wLastMinute) || (st.wHour != _wLastHour) || !*_szCurTime) { _wLastMinute = st.wMinute; _wLastHour = st.wHour; // // Text for the current time. // _cchCurTime = GetTimeFormat(LOCALE_USER_DEFAULT, TIME_NOSECONDS, &st, _szTimeFmt, _szCurTime, ARRAYSIZE(_szCurTime)); BOOL fRTL = IS_WINDOW_RTL_MIRRORED(_hwnd); _cchCurDate = GetDateFormat(LOCALE_USER_DEFAULT, fRTL ? DATE_RTLREADING : 0, &st, _szDateFmt, _szCurDate, ARRAYSIZE(_szCurDate)); _cchCurDay = GetDateFormat(LOCALE_USER_DEFAULT, fRTL ? DATE_RTLREADING : 0, &st, TEXT("dddd"), _szCurDay, ARRAYSIZE(_szCurDay)); // Don't count the NULL terminator. if (_cchCurTime > 0) _cchCurTime--; if (_cchCurDate > 0) _cchCurDate--; if (_cchCurDay > 0) _cchCurDay--; // // Update our window text so accessibility apps can see. Since we // don't have a caption USER won't try to paint us or anything, it // will just set the text and fire an event if any accessibility // clients are listening... // SetWindowText(_hwnd, _szCurTime); } // // Return number of milliseconds till we need to be called again. // return 1000UL * (60 - st.wSecond); } void CClockCtl::_EnsureFontsInitialized(BOOL fForce) { if (fForce || !_hfontCapNormal) { HFONT hfont; NONCLIENTMETRICS ncm; ncm.cbSize = sizeof(ncm); if (SystemParametersInfo(SPI_GETNONCLIENTMETRICS, sizeof(ncm), &ncm, 0)) { // Create the normal font ncm.lfCaptionFont.lfWeight = FW_NORMAL; hfont = CreateFontIndirect(&ncm.lfCaptionFont); if (hfont) { if (_hfontCapNormal) DeleteFont(_hfontCapNormal); _hfontCapNormal = hfont; } } } } void CClockCtl::_GetTextExtent(HDC hdc, TCHAR* pszText, int cchText, LPRECT prcText) { if (_hTheme) { GetThemeTextExtent(_hTheme, hdc, CLP_TIME, 0, pszText, cchText, 0, prcText, prcText); } else { SIZE size; GetTextExtentPoint(hdc, pszText, cchText, &size); SetRect(prcText, 0, 0, size.cx, size.cy); } } void CClockCtl::_DrawText(HDC hdc, TCHAR* pszText, int cchText, LPRECT prcText) { if (_hTheme) { DrawThemeText(_hTheme, hdc, CLP_TIME, 0, pszText, cchText, 0, 0, prcText); } else { ExtTextOut(hdc, prcText->left, prcText->top, ETO_OPAQUE, NULL, pszText, cchText, NULL); } } LRESULT CClockCtl::_DoPaint(BOOL fPaint) { PAINTSTRUCT ps; RECT rcClient, rcClip = {0}; DWORD dtNextTick = 0; BOOL fDoTimer; HDC hdc; HBITMAP hMemBm, hOldBm; // // If we are asked to paint and the clock is not running then start it. // Otherwise wait until we get a clock tick to recompute the time etc. // fDoTimer = !fPaint || !_fClockRunning; // // Get a DC to paint with. // if (fPaint) { BeginPaint(_hwnd, &ps); } else { ps.hdc = GetDC(_hwnd); GetClipBox(ps.hdc, &ps.rcPaint); } // Create memory surface and map rendering context if double buffering // Only make large enough for clipping region hdc = CreateCompatibleDC(ps.hdc); if (hdc) { hMemBm = CreateCompatibleBitmap(ps.hdc, RECTWIDTH(ps.rcPaint), RECTHEIGHT(ps.rcPaint)); if (hMemBm) { hOldBm = (HBITMAP) SelectObject(hdc, hMemBm); // Offset painting to paint in region OffsetWindowOrgEx(hdc, ps.rcPaint.left, ps.rcPaint.top, NULL); } else { DeleteDC(hdc); hdc = NULL; } } if (hdc) { SHSendPrintRect(GetParent(_hwnd), _hwnd, hdc, &ps.rcPaint); _EnsureFontsInitialized(FALSE); // // Update the time if we need to. // if (fDoTimer || !*_szCurTime) { dtNextTick = _RecalcCurTime(); ASSERT(dtNextTick); } // // Paint the clock face if we are not clipped or if we got a real // paint message for the window. We want to avoid turning off the // timer on paint messages (regardless of clip region) because this // implies the window is visible in some way. If we guessed wrong, we // will turn off the timer next timer tick anyway so no big deal. // if (GetClipBox(hdc, &rcClip) != NULLREGION || fPaint) { // // Draw the text centered in the window. // GetClientRect(_hwnd, &rcClient); HFONT hfontOld; if (_hfontCapNormal) hfontOld = SelectFont(hdc, _hfontCapNormal); SetBkColor(hdc, GetSysColor(COLOR_3DFACE)); SetTextColor(hdc, GetSysColor(COLOR_BTNTEXT)); BOOL fShowDate = FALSE; BOOL fShowDay = FALSE; RECT rcTime = {0}; RECT rcDate = {0}; RECT rcDay = {0}; _GetTextExtent(hdc, _szCurTime, _cchCurTime, &rcTime); _GetTextExtent(hdc, _szCurDate, _cchCurDate, &rcDate); _GetTextExtent(hdc, _szCurDay, _cchCurDay, &rcDay); int cySpace = RECTHEIGHT(rcTime) / 2; int cy = RECTHEIGHT(rcTime) + cySpace; if ((cy + RECTHEIGHT(rcDay) < rcClient.bottom) && (RECTWIDTH(rcDay) < rcClient.right)) { fShowDay = TRUE; cy += RECTHEIGHT(rcDay) + cySpace; if ((cy + RECTHEIGHT(rcDate) < rcClient.bottom) && (RECTWIDTH(rcDate) < rcClient.right)) { fShowDate = TRUE; cy += RECTHEIGHT(rcDate) + cySpace; } } cy -= cySpace; int yOffset = max((rcClient.bottom - cy) / 2, 0); RECT rcDraw = rcTime; OffsetRect(&rcDraw, max((rcClient.right - RECTWIDTH(rcTime)) / 2, 0), yOffset); _DrawText(hdc, _szCurTime, _cchCurTime, &rcDraw); yOffset += RECTHEIGHT(rcTime) + cySpace; if (fShowDay) { rcDraw = rcDay; OffsetRect(&rcDraw, max((rcClient.right - RECTWIDTH(rcDay)) / 2, 0), yOffset); _DrawText(hdc, _szCurDay, _cchCurDay, &rcDraw); yOffset += RECTHEIGHT(rcDay) + cySpace; if (fShowDate) { rcDraw = rcDate; OffsetRect(&rcDraw, max((rcClient.right - RECTWIDTH(rcDate)) / 2, 0), yOffset); _DrawText(hdc, _szCurDate, _cchCurDate, &rcDraw); } } // figure out if the time is clipped _fClockClipped = (RECTWIDTH(rcTime) > rcClient.right || RECTHEIGHT(rcTime) > rcClient.bottom); if (_hfontCapNormal) SelectObject(hdc, hfontOld); if (_fHasFocus) { LRESULT lRes = SendMessage(_hwnd, WM_QUERYUISTATE, 0, 0); if (!(LOWORD(lRes) & UISF_HIDEFOCUS)) { RECT rcFocus = rcClient; InflateRect(&rcFocus, -2, 0); DrawFocusRect(hdc, &rcFocus); } } } else { // // We are obscured so make sure we turn off the clock. // dtNextTick = 0; fDoTimer = TRUE; } BitBlt(ps.hdc, ps.rcPaint.left, ps.rcPaint.top, RECTWIDTH(ps.rcPaint), RECTHEIGHT(ps.rcPaint), hdc, ps.rcPaint.left, ps.rcPaint.top, SRCCOPY); SelectObject(hdc, hOldBm); DeleteObject(hMemBm); DeleteDC(hdc); // // Release our paint DC. // if (fPaint) EndPaint(_hwnd, &ps); else ReleaseDC(_hwnd, ps.hdc); } // // Reset/Kill the timer. // if (fDoTimer) { _EnableTimer(dtNextTick); // // If we just killed the timer because we were clipped when it arrived, // make sure that we are really clipped by invalidating ourselves once. // if (hdc) { if (!dtNextTick && !fPaint) InvalidateRect(_hwnd, NULL, FALSE); else { InvalidateRect(_hwnd, NULL, TRUE); } } } return 0; } void CClockCtl::_Reset() { // // Reset the clock by killing the timer and invalidating. // Everything will be updated when we try to paint. // _EnableTimer(0); InvalidateRect(_hwnd, NULL, FALSE); } LRESULT CClockCtl::_HandleTimeChange() { *_szCurTime = 0; // Force a text recalc. _UpdateLastHour(); _Reset(); return 1; } static const TCHAR c_szSlop[] = TEXT("00"); void CClockCtl::_GetMaxTimeSize(HDC hdc, LPSIZE pszTime) { SYSTEMTIME st={0}; // Initialize to 0... RECT rcAM = {0}; RECT rcPM = {0}; TCHAR szTime[40]; // We need to get the AM and the PM sizes... // We append Two 0s at end to add slop into size // first AM st.wHour=11; int cch = GetTimeFormat(LOCALE_USER_DEFAULT, TIME_NOSECONDS, &st, _szTimeFmt, szTime, ARRAYSIZE(szTime) - ARRAYSIZE(c_szSlop)); if (cch) cch--; // don't count the NULL StringCchCat(szTime, ARRAYSIZE(szTime), c_szSlop); _GetTextExtent(hdc, szTime, cch+2, &rcAM); // then PM st.wHour=23; cch = GetTimeFormat(LOCALE_USER_DEFAULT, TIME_NOSECONDS, &st, _szTimeFmt, szTime, ARRAYSIZE(szTime) - ARRAYSIZE(c_szSlop)); if (cch) cch--; // don't count the NULL StringCchCat(szTime, ARRAYSIZE(szTime), c_szSlop); _GetTextExtent(hdc, szTime, cch+2, &rcPM); pszTime->cx = max(rcAM.right, rcPM.right); pszTime->cy = max(rcAM.bottom, rcPM.bottom); } void CClockCtl::_GetMaxDateSize(HDC hdc, LPSIZE pszTime) { SYSTEMTIME st={0}; // Initialize to 0... TCHAR szDate[43]; st.wYear = 2001; st.wMonth = 5; st.wDay = 5; BOOL fRTL = IS_WINDOW_RTL_MIRRORED(_hwnd); int cch = GetDateFormat(LOCALE_USER_DEFAULT, fRTL ? DATE_RTLREADING : 0, &st, _szDateFmt, szDate, ARRAYSIZE(szDate) - ARRAYSIZE(c_szSlop)); if (cch > 0) cch--; // don't count the NULL StringCchCat(szDate, ARRAYSIZE(szDate), c_szSlop); RECT rc = {0}; _GetTextExtent(hdc, szDate, cch+2, &rc); pszTime->cx = rc.right; pszTime->cy = rc.bottom; } void CClockCtl::_GetMaxDaySize(HDC hdc, LPSIZE pszTime) { SYSTEMTIME st={0}; // Initialize to 0... TCHAR szDay[40]; pszTime->cx = 0; pszTime->cy = 0; // Use a fake date, otherwise GetDateFormat complains about invalid args // BTW, the date is the day I fixed this bug for those of you reading this comment // in the year 2025. st.wYear = 2001; st.wMonth = 3; for (WORD wDay = 1; wDay <= 7; wDay++) { st.wDay = wDay; int cch = GetDateFormat(LOCALE_USER_DEFAULT, 0, &st, TEXT("dddd"), szDay, ARRAYSIZE(szDay) - ARRAYSIZE(c_szSlop)); if (cch) cch--; // don't count the NULL StringCchCat(szDay, ARRAYSIZE(szDay), c_szSlop); RECT rc = {0}; _GetTextExtent(hdc, szDay, cch+2, &rc); pszTime->cx = max(pszTime->cx, rc.right); pszTime->cy = max(pszTime->cy, rc.bottom); } } LRESULT CClockCtl::_CalcMinSize(int cxMax, int cyMax) { RECT rc; HDC hdc; HFONT hfontOld; if (!(GetWindowLong(_hwnd, GWL_STYLE) & WS_VISIBLE)) return 0L; if (_szTimeFmt[0] == TEXT('\0')) { if (GetLocaleInfo(LOCALE_USER_DEFAULT, LOCALE_STIMEFORMAT, _szTimeFmt, ARRAYSIZE(_szTimeFmt)) == 0) { TraceMsg(TF_ERROR, "c.ccms: GetLocalInfo Failed %d.", GetLastError()); } *_szCurTime = 0; // Force the text to be recomputed. } if (_szDateFmt[0] == TEXT('\0')) { if (GetLocaleInfo(LOCALE_USER_DEFAULT, LOCALE_SSHORTDATE, _szDateFmt, ARRAYSIZE(_szDateFmt)) == 0) { TraceMsg(TF_ERROR, "c.ccms: GetLocalInfo Failed %d.", GetLastError()); } *_szCurDate = 0; // Force the text to be recomputed. } hdc = GetDC(_hwnd); if (!hdc) return(0L); _EnsureFontsInitialized(FALSE); if (_hfontCapNormal) hfontOld = SelectFont(hdc, _hfontCapNormal); SIZE size = {0}; SIZE sizeTemp = {0}; _GetMaxTimeSize(hdc, &sizeTemp); int cySpace = sizeTemp.cy / 2; size.cy += sizeTemp.cy; size.cx = max(sizeTemp.cx, size.cx); _GetMaxDaySize(hdc, &sizeTemp); if ((size.cy + sizeTemp.cy + cySpace < cyMax) && (sizeTemp.cx < cxMax)) { size.cy += sizeTemp.cy + cySpace; size.cx = max(sizeTemp.cx, size.cx); _GetMaxDateSize(hdc, &sizeTemp); if ((size.cy + sizeTemp.cy + cySpace < cyMax) && (sizeTemp.cx < cxMax)) { size.cy += sizeTemp.cy + cySpace; size.cx = max(sizeTemp.cx, size.cx); } } if (_hfontCapNormal) SelectObject(hdc, hfontOld); ReleaseDC(_hwnd, hdc); // Now lets set up our rectangle... // The width is 6 digits (a digit slop on both ends + size of // : or sep and max AM or PM string...) SetRect(&rc, 0, 0, size.cx, size.cy + 4 * g_cyBorder); AdjustWindowRectEx(&rc, GetWindowLong(_hwnd, GWL_STYLE), FALSE, GetWindowLong(_hwnd, GWL_EXSTYLE)); // make sure we're at least the size of other buttons: if (rc.bottom - rc.top < g_cySize + g_cyEdge) rc.bottom = rc.top + g_cySize + g_cyEdge; return MAKELRESULT((rc.right - rc.left), (rc.bottom - rc.top)); } LRESULT CClockCtl::_HandleIniChange(WPARAM wParam, LPTSTR pszSection) { if ((pszSection == NULL) || (lstrcmpi(pszSection, TEXT("WindowMetrics")) == 0) || wParam == SPI_SETNONCLIENTMETRICS) { _EnsureFontsInitialized(TRUE); } // Only process certain sections... if ((pszSection == NULL) || (lstrcmpi(pszSection, TEXT("intl")) == 0) || (wParam == SPI_SETICONTITLELOGFONT)) { TOOLINFO ti; _szTimeFmt[0] = TEXT('\0'); // Go reread the format. _szDateFmt[0] = TEXT('\0'); // Go reread the format. // And make sure we have it recalc... RECT rc; GetClientRect(_hwnd, &rc); // // When the time/locale is changed, we get a WM_WININICHANGE. // But the WM_WININICHANGE comes *AFTER* the "sizing" messages. By the time // we are here, we have calculated the min. size of the clock window based // on the *PREVIOUS* time. The tray sets the clock window size based on // this "previous" size, but NOW we get the WININICHANGE, and can calculate // the new size of the clock. So we have to tell the tray to change our // size now, and then redraw ourselves. c_tray.SizeWindows(); ti.cbSize = sizeof(ti); ti.uFlags = 0; ti.hwnd = v_hwndTray; ti.uId = (UINT_PTR)_hwnd; ti.lpszText = LPSTR_TEXTCALLBACK; SendMessage(c_tray.GetTrayTips(), TTM_UPDATETIPTEXT, 0, (LPARAM)&ti); _Reset(); } return 0; } LRESULT CClockCtl::_OnNeedText(LPTOOLTIPTEXT lpttt) { int iDateFormat = DATE_LONGDATE; // // This code is really squirly. We don't know if the time has been // clipped until we actually try to paint it, since the clip logic // is in the WM_PAINT handler... Go figure... // if (!*_szCurTime) { InvalidateRect(_hwnd, NULL, FALSE); UpdateWindow(_hwnd); } // // If the current user locale is any BiDi locale, then // Make the date reading order it RTL. SetBiDiDateFlags only adds // DATE_RTLREADING if the locale is BiDi. [samera] // SetBiDiDateFlags(&iDateFormat); if (_fClockClipped) { // we need to put the time in here too TCHAR sz[80]; GetDateFormat(LOCALE_USER_DEFAULT, iDateFormat, NULL, NULL, sz, ARRAYSIZE(sz)); StringCchPrintf(lpttt->szText, ARRAYSIZE(lpttt->szText), TEXT("%s %s"), _szCurTime, sz); } else { GetDateFormat(LOCALE_USER_DEFAULT, iDateFormat, NULL, NULL, lpttt->szText, ARRAYSIZE(lpttt->szText)); } return TRUE; } void CClockCtl::_HandleThemeChanged(WPARAM wParam) { if (_hTheme) { CloseThemeData(_hTheme); _hTheme = NULL; } if (wParam) { _hTheme = OpenThemeData(_hwnd, L"Clock"); } InvalidateRect(_hwnd, NULL, TRUE); } LRESULT CClockCtl::v_WndProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam) { switch (uMsg) { case WM_CALCMINSIZE: return _CalcMinSize((int)wParam, (int)lParam); case WM_NCCREATE: return _HandleCreate(); case WM_NCDESTROY: return _HandleDestroy(); case WM_ERASEBKGND: return 1; case WM_TIMER: case WM_PAINT: return _DoPaint((uMsg == WM_PAINT)); case WM_WININICHANGE: return _HandleIniChange(wParam, (LPTSTR)lParam); case WM_POWER: // // a critical resume does not generate a WM_POWERBROADCAST // to windows for some reason, but it does generate a old // WM_POWER message. // if (wParam != PWR_CRITICALRESUME) break; // // Fall through... // case WM_TIMECHANGE: return _HandleTimeChange(); case WM_NCHITTEST: return(HTTRANSPARENT); case WM_SHOWWINDOW: if (wParam) break; // fall through case TCM_RESET: _Reset(); break; case WM_NOTIFY: { NMHDR *pnm = (NMHDR*)lParam; switch (pnm->code) { case TTN_NEEDTEXT: return _OnNeedText((LPTOOLTIPTEXT)lParam); break; } break; } case WM_THEMECHANGED: _HandleThemeChanged(wParam); break; case WM_SETFOCUS: case WM_KILLFOCUS: _fHasFocus = (uMsg == WM_SETFOCUS); InvalidateRect(_hwnd, NULL, TRUE); break; case WM_KEYDOWN: case WM_KEYUP: case WM_CHAR: case WM_SYSKEYDOWN: case WM_SYSKEYUP: case WM_SYSCHAR: // // forward all keyboard input to parent // if (SendMessage(GetParent(_hwnd), uMsg, wParam, lParam) == 0) { // The message has been handled... break; } // // else Fall through... // case WM_GETTEXT: // // Update the text if we are not running and somebody wants it. // if (uMsg == WM_GETTEXT) { if (!_fClockRunning) _RecalcCurTime(); } default: return DefWindowProc(hwnd, uMsg, wParam, lParam); } return 0; } // Register the clock class. BOOL ClockCtl_Class(HINSTANCE hinst) { WNDCLASS wc = {0}; wc.lpszClassName = WC_TRAYCLOCK; wc.style = CS_HREDRAW | CS_VREDRAW; wc.lpfnWndProc = CClockCtl::s_WndProc; wc.hInstance = hinst; wc.hCursor = LoadCursor(NULL, IDC_ARROW); wc.hbrBackground = (HBRUSH)(COLOR_3DFACE + 1); wc.cbWndExtra = sizeof(CClockCtl*); return RegisterClass(&wc); } HWND ClockCtl_Create(HWND hwndParent, UINT uID, HINSTANCE hInst) { HWND hwnd = NULL; CClockCtl* pcc = new CClockCtl(); if (pcc) { hwnd = CreateWindowEx(0, WC_TRAYCLOCK, NULL, WS_CHILD | WS_CLIPSIBLINGS | WS_VISIBLE, 0, 0, 0, 0, hwndParent, IntToPtr_(HMENU, uID), hInst, pcc); pcc->Release(); } return hwnd; }