/* Miranda NG: the free IM client for Microsoft* Windows* Copyright (C) 2012-23 Miranda NG team (https://miranda-ng.org), Copyright (c) 2000-08 Miranda ICQ/IM project, all portions of this codebase are copyrighted to the people listed in contributors.txt. This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. */ // File contains implementation of animated avatars in contact list #include "stdafx.h" #define IMMEDIATE_DRAW (!s_bSeparateWindow) void GDIPlus_ExtractAnimatedGIF(wchar_t *szName, int width, int height, HBITMAP &pBmp, int* &pframesDelay, int &pframesCount, SIZE &sizeAvatar); BOOL GDIPlus_IsAnimatedGif(wchar_t *szName); /* Next is module */ #define ANIAVAWINDOWCLASS L"MirandaModernAniAvatar" #define aacheck if (!s_bModuleStarted) return #define AAO_HAS_BORDER 0x01 #define AAO_ROUND_CORNERS 0x02 #define AAO_HAS_OVERLAY 0x04 #define AAO_OPAQUE 0x08 //messages enum { AAM_FIRST = WM_USER, AAM_SETAVATAR, //sync WPARAM: wchar_t * filename, LPARAM: SIZE * size, RESULT: actual size AAM_SETPOSITION, //async LPARAM: pointer to set pos info - the handler will empty it, RESULT: 0 AAM_REDRAW, //async AAM_STOP, //async stops animation, timer, hide window - prepeare for deleting AAM_PAUSE, //sync keep timer and window, but do not process painting -need before graphics change AAM_RESUME, //async remobe previous flag. repaints if required AAM_REMOVEAVATAR, //sync WPARAM: if y more then wParam, LPARAM: shift up to lParam( remove if values is same) AAM_SETPARENT, //async WPARAM: handle of new parent window AAM_RENDER, //sync AAM_LAST, }; struct ANIAVA_OBJECT : public MZeroedObject { MCONTACT hContact; HWND hWindow; BOOL bInvalidPos; uint32_t dwAvatarUniqId; SIZE ObjectSize; ~ANIAVA_OBJECT() { if (hWindow) DestroyWindow(hWindow); } }; struct ANIAVA_INFO { uint32_t dwAvatarUniqId; wchar_t *tcsFilename; int nRefCount; int nStripTop; int nFrameCount; int *pFrameDelays; SIZE FrameSize; }; struct ANIAVA_WINDOWINFO { HWND hWindow; RECT rcPos; SIZE sizeAvatar; BOOL StopTimer; int TimerId; int nFramesCount; int *delaysInterval; int currentFrame; POINT ptFromPoint; BOOL bPlaying; int overlayIconIdx; uint8_t bAlpha; BOOL bOrderTop; BOOL bPaused; // was request do not draw BOOL bPended; // till do not draw - was painting - need to be repaint }; struct ANIAVA_POSINFO { RECT rcPos; int idxOverlay; uint8_t bAlpha; }; struct ANIAVA_SYNCCALLITEM { WPARAM wParam; LPARAM lParam; INT_PTR nResult; HANDLE hDoneEvent; PSYNCCALLBACKPROC pfnProc; }; struct ANIAVATARIMAGEINFO { POINT ptImagePos; int nFramesCount; int *pFrameDelays; SIZE szSize; }; // protection static BOOL s_bModuleStarted; static mir_cs s_CS; // options static uint8_t s_bFlags; // 0x1 has border, 0x2 has round corners, 0x4 has overlay, 0x8 background color static uint8_t s_cornerRadius; static COLORREF s_borderColor; static COLORREF s_bkgColor; static HIMAGELIST s_overlayIconImageList; // animations static HDC s_hAniAvaDC; static HBITMAP s_hAniAvaBitmap; static HBITMAP s_hAniAvaOldBitmap; static int s_width, s_height; static UINT s_AnimationThreadID; static HANDLE s_AnimationThreadHandle; static HANDLE s_hExitEvent; ///////////////////////////////////////////////////////////////////////////////////////// static int _AniAva_SortAvatarInfo(const ANIAVA_INFO *aai1, const ANIAVA_INFO *aai2) { int res; if (aai1 && aai1->tcsFilename && aai2 && aai2->tcsFilename) res = mir_wstrcmpi(aai2->tcsFilename, aai1->tcsFilename); else { int a1 = (aai1 != nullptr && aai1->tcsFilename != nullptr); int a2 = (aai2 != nullptr && aai2->tcsFilename != nullptr); res = a1 - a2; } if (res) return res; return !(aai1->FrameSize.cx == aai2->FrameSize.cx && aai1->FrameSize.cy == aai2->FrameSize.cy); } static LIST s_AniAvatarList(1, _AniAva_SortAvatarInfo); // Objects static OBJLIST s_Objects(2, NumericKeySortT); static BOOL s_bSeparateWindow; ///////////////////////////////////////////////////////////////////////////////////////// /// IMPLEMENTATION static void _AniAva_AnimationTreadProc(void*) { Netlib_Logf(nullptr, "AnimationTreadProc thread start"); // wait forever till hExitEvent signalled HANDLE hThread = nullptr; s_AnimationThreadID = GetCurrentThreadId(); DuplicateHandle(GetCurrentProcess(), GetCurrentThread(), GetCurrentProcess(), &hThread, 0, FALSE, DUPLICATE_SAME_ACCESS); s_AnimationThreadHandle = hThread; SetThreadPriority(hThread, THREAD_PRIORITY_LOWEST); for (;;) { uint32_t rc = MsgWaitForMultipleObjectsEx(1, &s_hExitEvent, INFINITE, QS_ALLINPUT, MWMO_ALERTABLE); if (MirandaExiting()) break; ResetEvent(s_hExitEvent); if (rc == WAIT_OBJECT_0 + 1) { MSG msg; while (PeekMessage(&msg, nullptr, 0, 0, PM_REMOVE)) { if (msg.hwnd != nullptr && IsDialogMessage(msg.hwnd, &msg)) /* Wine fix. */ continue; TranslateMessage(&msg); DispatchMessage(&msg); } } else if (rc == WAIT_OBJECT_0) break; } Netlib_Logf(nullptr, "AnimationTreadProc thread end"); CloseHandle(s_AnimationThreadHandle); s_AnimationThreadHandle = nullptr; CloseHandle(s_hExitEvent); s_hExitEvent = nullptr; } ///////////////////////////////////////////////////////////////////////////////////////// // Static procedures __forceinline ANIAVA_OBJECT* FindAvatarByContact(MCONTACT hContact) { return s_Objects.find((ANIAVA_OBJECT*)&hContact); } static void CALLBACK _AniAva_SyncCallerUserAPCProc(DWORD_PTR dwParam) { ANIAVA_SYNCCALLITEM *item = (ANIAVA_SYNCCALLITEM*)dwParam; item->nResult = item->pfnProc(item->wParam, item->lParam); SetEvent(item->hDoneEvent); } static INT_PTR _AniAva_CreateAvatarWindowSync_Worker(WPARAM tszName, LPARAM) { HWND hwnd = CreateWindowEx(WS_EX_TOPMOST | WS_EX_TRANSPARENT | WS_EX_NOPARENTNOTIFY, ANIAVAWINDOWCLASS, (wchar_t*)tszName, WS_POPUP, 0, 0, 1, 1, g_clistApi.hwndContactList, nullptr, g_clistApi.hInst, nullptr); return (INT_PTR)hwnd; } static HWND _AniAva_CreateAvatarWindowSync(wchar_t *szFileName) { if (s_AnimationThreadHandle == nullptr || s_AnimationThreadID == 0) return nullptr; ANIAVA_SYNCCALLITEM item = { 0 }; item.wParam = (WPARAM)szFileName; item.pfnProc = _AniAva_CreateAvatarWindowSync_Worker; item.hDoneEvent = CreateEvent(nullptr, FALSE, FALSE, nullptr); if (GetCurrentThreadId() != s_AnimationThreadID) QueueUserAPC(_AniAva_SyncCallerUserAPCProc, s_AnimationThreadHandle, (DWORD_PTR)&item); else _AniAva_SyncCallerUserAPCProc((DWORD_PTR)&item); WaitForSingleObject(item.hDoneEvent, INFINITE); CloseHandle(item.hDoneEvent); return (HWND)item.nResult; } static void _AniAva_PausePainting() { for (auto &it : s_Objects) SendMessage(it->hWindow, AAM_PAUSE, 0, 0); } static void _AniAva_ResumePainting() { for (auto &it : s_Objects) SendNotifyMessage(it->hWindow, AAM_RESUME, 0, 0); } static void _AniAva_ReduceAvatarImages(int startY, int dY, BOOL bDestroyWindow) { for (auto &it : s_Objects.rev_iter()) { int res = SendMessage(it->hWindow, AAM_REMOVEAVATAR, (WPARAM)startY, (LPARAM)dY); if (res == 0xDEAD && bDestroyWindow) s_Objects.removeItem(&it); } } static void _AniAva_RemoveAniAvaDC() { if (s_hAniAvaDC) { SelectObject(s_hAniAvaDC, s_hAniAvaOldBitmap); DeleteObject(s_hAniAvaBitmap); DeleteDC(s_hAniAvaDC); s_hAniAvaDC = nullptr; s_height = 0; s_width = 0; s_hAniAvaBitmap = nullptr; } } static void _AniAva_RealRemoveAvatar(uint32_t UniqueID) { for (int j = 0; j < s_AniAvatarList.getCount(); j++) { ANIAVA_INFO *aai = s_AniAvatarList[j]; if (aai->dwAvatarUniqId == UniqueID) { aai->nRefCount--; if (aai->nRefCount == 0) { _AniAva_PausePainting(); mir_free(aai->tcsFilename); free(aai->pFrameDelays); _AniAva_ReduceAvatarImages(aai->nStripTop, aai->FrameSize.cx*aai->nFrameCount, FALSE); for (int k = 0; k < s_AniAvatarList.getCount(); k++) { if (k != j) { ANIAVA_INFO *taai = s_AniAvatarList[k]; if (taai->nStripTop>aai->nStripTop) taai->nStripTop -= aai->FrameSize.cx*aai->nFrameCount; } } if (s_AniAvatarList.getCount() > 0) { int newWidth = s_width - aai->FrameSize.cx*aai->nFrameCount; int newHeight = 0; for (int i = 0; i < s_AniAvatarList.getCount(); i++) if (i != j) newHeight = max(newHeight, s_AniAvatarList[i]->FrameSize.cy); HDC hNewDC = CreateCompatibleDC(nullptr); HBITMAP hNewBmp = ske_CreateDIB32(newWidth, newHeight); HBITMAP hNewOldBmp = (HBITMAP)SelectObject(hNewDC, hNewBmp); // copy from old and from new strip if (aai->nStripTop>0) BitBlt(hNewDC, 0, 0, aai->nStripTop, newHeight, s_hAniAvaDC, 0, 0, SRCCOPY); if (aai->nStripTop + aai->FrameSize.cx*aai->nFrameCount < s_width) BitBlt(hNewDC, aai->nStripTop, 0, s_width - (aai->nStripTop + aai->FrameSize.cx*aai->nFrameCount), newHeight, s_hAniAvaDC, aai->nStripTop + aai->FrameSize.cx*aai->nFrameCount, 0, SRCCOPY); _AniAva_RemoveAniAvaDC(); s_width = newWidth; s_height = newHeight; s_hAniAvaDC = hNewDC; s_hAniAvaBitmap = hNewBmp; s_hAniAvaOldBitmap = hNewOldBmp; } else _AniAva_RemoveAniAvaDC(); s_AniAvatarList.remove(j); mir_free(aai); _AniAva_ResumePainting(); break; } } } } static int _AniAva_LoadAvatarFromImage(wchar_t * szFileName, int width, int height, ANIAVATARIMAGEINFO *pRetAII) { ANIAVA_INFO aai = { 0 }; aai.tcsFilename = szFileName; aai.FrameSize.cx = width; aai.FrameSize.cy = height; ANIAVA_INFO *paai; int idx = s_AniAvatarList.getIndex(&aai); if (idx == -1) { //item not present in list paai = (ANIAVA_INFO *)mir_calloc(sizeof(ANIAVA_INFO)); paai->tcsFilename = mir_wstrdup(szFileName); paai->dwAvatarUniqId = rand(); // get image strip HBITMAP hBitmap = nullptr; GDIPlus_ExtractAnimatedGIF(szFileName, width, height, hBitmap, paai->pFrameDelays, paai->nFrameCount, paai->FrameSize); // copy image to temp DC HDC hTempDC = CreateCompatibleDC(nullptr); HBITMAP hOldBitmap = (HBITMAP)SelectObject(hTempDC, hBitmap); int newWidth = s_width + paai->FrameSize.cx*paai->nFrameCount; int newHeight = max(paai->FrameSize.cy, s_height); HDC hNewDC = CreateCompatibleDC(nullptr); HBITMAP hNewBmp = ske_CreateDIB32(newWidth, newHeight); HBITMAP hNewOldBmp = (HBITMAP)SelectObject(hNewDC, hNewBmp); _AniAva_PausePainting(); GdiFlush(); // copy from old and from new strip BitBlt(hNewDC, 0, 0, s_width, s_height, s_hAniAvaDC, 0, 0, SRCCOPY); BitBlt(hNewDC, s_width, 0, paai->FrameSize.cx*paai->nFrameCount, paai->FrameSize.cy, hTempDC, 0, 0, SRCCOPY); paai->nStripTop = s_width; s_AniAvatarList.insert(paai); GdiFlush(); // remove temp DC SelectObject(hTempDC, hOldBitmap); DeleteObject(hNewBmp); DeleteDC(hTempDC); DeleteObject(hBitmap); // delete old _AniAva_RemoveAniAvaDC(); // setNewDC; s_width = newWidth; s_height = newHeight; s_hAniAvaDC = hNewDC; s_hAniAvaBitmap = hNewBmp; s_hAniAvaOldBitmap = hNewOldBmp; GdiFlush(); _AniAva_ResumePainting(); } else paai = s_AniAvatarList[idx]; if (paai == nullptr) return 0; paai->nRefCount++; pRetAII->nFramesCount = paai->nFrameCount; pRetAII->pFrameDelays = paai->pFrameDelays; pRetAII->ptImagePos.x = paai->nStripTop; pRetAII->ptImagePos.y = 0; pRetAII->szSize = paai->FrameSize; return paai->dwAvatarUniqId; } static BOOL _AniAva_GetAvatarImageInfo(uint32_t dwAvatarUniqId, ANIAVATARIMAGEINFO * avii) { BOOL res = FALSE; for (auto &aai : s_AniAvatarList) { if (aai->dwAvatarUniqId == dwAvatarUniqId) { avii->nFramesCount = aai->nFrameCount; avii->pFrameDelays = aai->pFrameDelays; avii->ptImagePos.x = aai->nStripTop; avii->ptImagePos.y = 0; avii->szSize = aai->FrameSize; res = TRUE; break; } } return res; } static void _AniAva_Clear_ANIAVA_WINDOWINFO(ANIAVA_WINDOWINFO *pavwi) { pavwi->delaysInterval = nullptr; pavwi->nFramesCount = 0; KillTimer(pavwi->hWindow, 2); pavwi->bPlaying = FALSE; pavwi->TimerId = 0; } static void _AniAva_LoadOptions() { aacheck; mir_cslock lck(s_CS); s_bFlags = (g_plugin.getByte("AvatarsDrawBorders", SETTINGS_AVATARDRAWBORDER_DEFAULT) ? AAO_HAS_BORDER : 0) | (g_plugin.getByte("AvatarsRoundCorners", SETTINGS_AVATARROUNDCORNERS_DEFAULT) ? AAO_ROUND_CORNERS : 0) | (g_plugin.getByte("AvatarsDrawOverlay", SETTINGS_AVATARDRAWOVERLAY_DEFAULT) ? AAO_HAS_OVERLAY : 0) | ((0) ? AAO_OPAQUE : 0); if (s_bFlags & AAO_HAS_BORDER) s_borderColor = (COLORREF)g_plugin.getDword("AvatarsBorderColor", SETTINGS_AVATARBORDERCOLOR_DEFAULT); if (s_bFlags & AAO_ROUND_CORNERS) s_cornerRadius = g_plugin.getByte("AvatarsUseCustomCornerSize", SETTINGS_AVATARUSECUTOMCORNERSIZE_DEFAULT) ? g_plugin.getWord("AvatarsCustomCornerSize", SETTINGS_AVATARCORNERSIZE_DEFAULT) : 0; if (s_bFlags & AAO_HAS_OVERLAY) { // check image list uint8_t type = g_plugin.getByte("AvatarsOverlayType", SETTINGS_AVATAROVERLAYTYPE_DEFAULT); switch (type) { case SETTING_AVATAR_OVERLAY_TYPE_NORMAL: s_overlayIconImageList = hAvatarOverlays; break; case SETTING_AVATAR_OVERLAY_TYPE_PROTOCOL: case SETTING_AVATAR_OVERLAY_TYPE_CONTACT: s_overlayIconImageList = g_himlCListClc; break; default: s_overlayIconImageList = nullptr; } } if (s_bFlags & AAO_OPAQUE) s_bkgColor = 0; s_bSeparateWindow = g_plugin.getByte("AvatarsInSeparateWnd", SETTINGS_AVATARINSEPARATE_DEFAULT); } static void _AniAva_InvalidateParent(ANIAVA_WINDOWINFO * dat) { if (!IMMEDIATE_DRAW) return; HWND hwndParent = g_clistApi.hwndContactTree; RECT rcPos = dat->rcPos; g_clistApi.pfnInvalidateRect(hwndParent, &rcPos, FALSE); } static void _AniAva_RenderAvatar(ANIAVA_WINDOWINFO * dat, HDC hdcParent = nullptr, RECT *rcInParent = nullptr) { if (dat->bPaused > 0) { dat->bPended = TRUE; return; } else dat->bPended = FALSE; if (IMMEDIATE_DRAW && hdcParent == nullptr) return; GdiFlush(); if (dat->bPlaying && IsWindowVisible(dat->hWindow)) { POINT ptWnd = { 0 }; SIZE szWnd = { dat->rcPos.right - dat->rcPos.left, dat->rcPos.bottom - dat->rcPos.top }; BLENDFUNCTION bf = { AC_SRC_OVER, 0, uint8_t(g_CluiData.bCurrentAlpha * dat->bAlpha / 256), AC_SRC_ALPHA }; POINT pt_from = { 0, 0 }; HDC hDC_animation = GetDC(nullptr); HDC copyFromDC; RECT clistRect; HDC tempDC = nullptr; HBITMAP hBmp = nullptr; HBITMAP hOldBmp = nullptr; if (s_bFlags == 0) { //simple and fastest method - no borders, round corners and etc. just copy pt_from.x = dat->ptFromPoint.x + dat->currentFrame*dat->sizeAvatar.cx; pt_from.y = dat->ptFromPoint.y; copyFromDC = s_hAniAvaDC; } else { // ... need to create additional hDC_animation HRGN hRgn = nullptr; int cornerRadius = s_cornerRadius; tempDC = CreateCompatibleDC(nullptr); hBmp = ske_CreateDIB32(szWnd.cx, szWnd.cy); hOldBmp = (HBITMAP)SelectObject(tempDC, hBmp); if (s_bFlags & AAO_ROUND_CORNERS) if (!cornerRadius) //auto radius cornerRadius = min(szWnd.cx, szWnd.cy) / 5; if (s_bFlags & AAO_HAS_BORDER) { // if has borders - create region (round corners) and fill it, remember internal as clipping HBRUSH hBrush = CreateSolidBrush(s_borderColor); HBRUSH hOldBrush = (HBRUSH)SelectObject(tempDC, hBrush); HRGN rgnOutside = CreateRoundRectRgn(0, 0, szWnd.cx + 1, szWnd.cy + 1, cornerRadius << 1, cornerRadius << 1); hRgn = CreateRoundRectRgn(1, 1, szWnd.cx, szWnd.cy, cornerRadius << 1, cornerRadius << 1); CombineRgn(rgnOutside, rgnOutside, hRgn, RGN_DIFF); FillRgn(tempDC, rgnOutside, hBrush); ske_SetRgnOpaque(tempDC, rgnOutside, TRUE); SelectObject(tempDC, hOldBrush); DeleteObject(hBrush); DeleteObject(rgnOutside); } else if (cornerRadius > 0) // else create clipping area (round corners) hRgn = CreateRoundRectRgn(0, 0, szWnd.cx + 1, szWnd.cy + 1, cornerRadius << 1, cornerRadius << 1); else hRgn = CreateRectRgn(0, 0, szWnd.cx + 1, szWnd.cy + 1); // select clip area if (hRgn) ExtSelectClipRgn(tempDC, hRgn, RGN_AND); if (s_bFlags & AAO_OPAQUE) { // if back color - fill clipping area HBRUSH hBrush = CreateSolidBrush(s_bkgColor); FillRgn(tempDC, hRgn, hBrush); ske_SetRgnOpaque(tempDC, hRgn, TRUE); } // draw avatar if (!(s_bFlags & AAO_OPAQUE)) BitBlt(tempDC, 0, 0, szWnd.cx, szWnd.cy, s_hAniAvaDC, dat->ptFromPoint.x + dat->sizeAvatar.cx*dat->currentFrame, dat->ptFromPoint.y, SRCCOPY); else { BLENDFUNCTION abf = { AC_SRC_OVER, 0, 255, AC_SRC_ALPHA }; ske_AlphaBlend(tempDC, 0, 0, szWnd.cx, szWnd.cy, s_hAniAvaDC, dat->ptFromPoint.x + dat->sizeAvatar.cx*dat->currentFrame, dat->ptFromPoint.y, szWnd.cx, szWnd.cy, abf); } // reset clip area if (hRgn) { DeleteObject(hRgn); hRgn = CreateRectRgn(0, 0, szWnd.cx, szWnd.cy); SelectClipRgn(tempDC, hRgn); DeleteObject(hRgn); } if ((s_bFlags & AAO_HAS_OVERLAY) && dat->overlayIconIdx != -1 && s_overlayIconImageList) { // if overlay - draw overlay icon // position - on avatar int x = szWnd.cx - ICON_WIDTH; int y = szWnd.cy - ICON_HEIGHT; ske_ImageList_DrawEx(s_overlayIconImageList, dat->overlayIconIdx & 0xFFFF, tempDC, x, y, ICON_WIDTH, ICON_HEIGHT, CLR_NONE, CLR_NONE, ILD_NORMAL); } copyFromDC = tempDC; } // intersect visible area // update layered window GetWindowRect(g_clistApi.hwndContactTree, &clistRect); if (dat->rcPos.top < 0) { pt_from.y += -dat->rcPos.top; szWnd.cy += dat->rcPos.top; } if (dat->rcPos.bottom>clistRect.bottom - clistRect.top) szWnd.cy -= (dat->rcPos.bottom - (clistRect.bottom - clistRect.top)); ptWnd.x = dat->rcPos.left + clistRect.left; ptWnd.y = (dat->rcPos.top > 0 ? dat->rcPos.top : 0) + clistRect.top; if (szWnd.cy > 0) { if (hdcParent && rcInParent && IMMEDIATE_DRAW) { if (s_bFlags & AAO_OPAQUE) BitBlt(hdcParent, rcInParent->left, rcInParent->top, szWnd.cx, szWnd.cy, copyFromDC, pt_from.x, pt_from.y, SRCCOPY); else { BLENDFUNCTION abf = { AC_SRC_OVER, 0, 255, AC_SRC_ALPHA }; ske_AlphaBlend(hdcParent, rcInParent->left, rcInParent->top, szWnd.cx, szWnd.cy, copyFromDC, pt_from.x, pt_from.y, szWnd.cx, szWnd.cy, abf); } } else if (!UpdateLayeredWindow(dat->hWindow, hDC_animation, &ptWnd, &szWnd, copyFromDC, &pt_from, RGB(0, 0, 0), &bf, ULW_ALPHA)) { LONG_PTR exStyle; exStyle = GetWindowLongPtr(dat->hWindow, GWL_EXSTYLE); exStyle |= WS_EX_LAYERED; SetWindowLongPtr(dat->hWindow, GWL_EXSTYLE, exStyle); if (!IMMEDIATE_DRAW) SetWindowPos(g_clistApi.hwndContactTree, dat->hWindow, 0, 0, 0, 0, SWP_ASYNCWINDOWPOS | SWP_NOSIZE | SWP_NOMOVE | SWP_NOACTIVATE | SWP_NOSENDCHANGING); UpdateLayeredWindow(dat->hWindow, hDC_animation, &ptWnd, &szWnd, copyFromDC, &pt_from, RGB(0, 0, 0), &bf, ULW_ALPHA); } g_CluiData.fAeroGlass = false; CLUI_UpdateAeroGlass(); } else dat->bPlaying = FALSE; ReleaseDC(nullptr, hDC_animation); if (tempDC) { SelectObject(tempDC, hOldBmp); DeleteObject(hBmp); DeleteDC(tempDC); } } if (!dat->bPlaying) { ShowWindow(dat->hWindow, SW_HIDE); KillTimer(dat->hWindow, 2); //stop animation till set pos will be called } GdiFlush(); } static LRESULT CALLBACK _AniAva_WndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam) { ANIAVA_WINDOWINFO *dat = nullptr; if (msg == WM_TIMER || msg == WM_DESTROY || (msg > AAM_FIRST && msg < AAM_LAST)) dat = (ANIAVA_WINDOWINFO *)GetWindowLongPtr(hwnd, GWLP_USERDATA); switch (msg) { case AAM_REMOVEAVATAR: if (dat->ptFromPoint.x == (int)wParam) return 0xDEAD; //need to destroy window else if (dat->ptFromPoint.x>(int)wParam) dat->ptFromPoint.x -= (int)lParam; return 0; case AAM_PAUSE: dat->bPaused++; return 0; case AAM_RESUME: dat->bPaused--; if (dat->bPaused) return 0; if (dat->bPended) if (!IMMEDIATE_DRAW) _AniAva_RenderAvatar(dat); dat->bPended = FALSE; return 0; case AAM_STOP: if (dat->bPlaying) { dat->bPlaying = FALSE; KillTimer(hwnd, 2); ShowWindow(hwnd, SW_HIDE); } return 0; case AAM_SETAVATAR: _AniAva_Clear_ANIAVA_WINDOWINFO(dat); { ANIAVATARIMAGEINFO *paaii = (ANIAVATARIMAGEINFO*)wParam; dat->nFramesCount = paaii->nFramesCount; dat->delaysInterval = paaii->pFrameDelays; dat->sizeAvatar = paaii->szSize; dat->ptFromPoint = paaii->ptImagePos; } dat->currentFrame = 0; dat->bPlaying = FALSE; return MAKELONG(dat->sizeAvatar.cx, dat->sizeAvatar.cy); case AAM_SETPOSITION: if (dat->delaysInterval) { ANIAVA_POSINFO *papi = (ANIAVA_POSINFO*)lParam; if (papi != nullptr) { dat->rcPos = papi->rcPos; dat->overlayIconIdx = papi->idxOverlay; dat->bAlpha = papi->bAlpha; free(papi); if (!dat->bPlaying) { dat->bPlaying = TRUE; ShowWindow(hwnd, SW_SHOWNA); dat->currentFrame = 0; KillTimer(hwnd, 2); SetTimer(hwnd, 2, dat->delaysInterval[0], nullptr); } if (!IMMEDIATE_DRAW) _AniAva_RenderAvatar(dat); } } return 0; case AAM_SETPARENT: if (IMMEDIATE_DRAW) return 0; dat->bOrderTop = ((HWND)wParam != GetDesktopWindow()); SetParent(hwnd, (HWND)wParam); if (dat->bOrderTop) SetWindowPos(hwnd, HWND_TOP, 0, 0, 0, 0, SWP_NOSIZE | SWP_NOMOVE | SWP_NOACTIVATE | SWP_ASYNCWINDOWPOS); else { LONG exStyle = GetWindowLongPtr(g_clistApi.hwndContactList, GWL_EXSTYLE); SetWindowPos(g_clistApi.hwndContactList, hwnd, 0, 0, 0, 0, SWP_NOSIZE | SWP_NOMOVE | SWP_NOACTIVATE); if (!(exStyle & WS_EX_TOPMOST)) SetWindowPos(g_clistApi.hwndContactList, HWND_NOTOPMOST, 0, 0, 0, 0, SWP_NOSIZE | SWP_NOMOVE | SWP_NOACTIVATE); } return 0; case AAM_REDRAW: if (IMMEDIATE_DRAW) return 0; if (wParam) { if (dat->bOrderTop) SetWindowPos(hwnd, HWND_TOP, 0, 0, 0, 0, SWP_NOSIZE | SWP_NOMOVE | SWP_NOACTIVATE | SWP_ASYNCWINDOWPOS); else { LONG exStyle = GetWindowLongPtr(g_clistApi.hwndContactList, GWL_EXSTYLE); SetWindowPos(g_clistApi.hwndContactList, hwnd, 0, 0, 0, 0, SWP_NOSIZE | SWP_NOMOVE | SWP_NOACTIVATE); if (!(exStyle&WS_EX_TOPMOST)) SetWindowPos(g_clistApi.hwndContactList, HWND_NOTOPMOST, 0, 0, 0, 0, SWP_NOSIZE | SWP_NOMOVE | SWP_NOACTIVATE); } } _AniAva_RenderAvatar(dat); return 0; case AAM_RENDER: _AniAva_RenderAvatar(dat, (HDC)wParam, (RECT*)lParam); return 0; case WM_CREATE: dat = (ANIAVA_WINDOWINFO *)mir_calloc(sizeof(ANIAVA_WINDOWINFO)); SetWindowLongPtr(hwnd, GWLP_USERDATA, (LONG_PTR)dat); dat->hWindow = hwnd; // change layered mode { LONG_PTR exStyle = GetWindowLongPtr(dat->hWindow, GWL_EXSTYLE); exStyle |= WS_EX_LAYERED; SetWindowLongPtr(dat->hWindow, GWL_EXSTYLE, exStyle); exStyle = GetWindowLongPtr(dat->hWindow, GWL_STYLE); exStyle &= ~WS_POPUP; exStyle |= WS_CHILD; SetWindowLongPtr(dat->hWindow, GWL_STYLE, exStyle); } break; case WM_TIMER: if (!IsWindowVisible(hwnd)) { DestroyWindow(hwnd); return 0; } dat->currentFrame++; if (dat->currentFrame >= dat->nFramesCount) dat->currentFrame = 0; if (!IMMEDIATE_DRAW) _AniAva_RenderAvatar(dat); else _AniAva_InvalidateParent(dat); KillTimer(hwnd, 2); SetTimer(hwnd, 2, dat->delaysInterval[dat->currentFrame] + 1, nullptr); return 0; case WM_DESTROY: _AniAva_Clear_ANIAVA_WINDOWINFO(dat); mir_free(dat); SetWindowLongPtr(hwnd, GWLP_USERDATA, 0); break; } return DefWindowProc(hwnd, msg, wParam, lParam); } ///////////////////////////////////////////////////////////////////////////////////////// // public functions //======================================================================================= // adds avatars to be displayed int AniAva_AddAvatar(MCONTACT hContact, wchar_t * szFilename, int width, int heigth) { aacheck 0; if (!GDIPlus_IsAnimatedGif(szFilename)) return 0; mir_cslock lck(s_CS); // first try to find window for contact avatar HWND hwnd = nullptr; ANIAVA_OBJECT *pavi = FindAvatarByContact(hContact); if (pavi != nullptr) { if (pavi->ObjectSize.cx == width && pavi->ObjectSize.cy == heigth) hwnd = pavi->hWindow; else { DestroyWindow(pavi->hWindow); pavi->hWindow = nullptr; _AniAva_RealRemoveAvatar(pavi->dwAvatarUniqId); pavi->dwAvatarUniqId = 0; } } else { pavi = new ANIAVA_OBJECT(); pavi->hContact = hContact; s_Objects.insert(pavi); } // change avatar pavi->bInvalidPos = 0; // now CreateAvatar ANIAVATARIMAGEINFO avii = { 0 }; if (pavi->dwAvatarUniqId) _AniAva_GetAvatarImageInfo(pavi->dwAvatarUniqId, &avii); else pavi->dwAvatarUniqId = _AniAva_LoadAvatarFromImage(szFilename, width, heigth, &avii); if (hwnd) SendMessage(hwnd, AAM_SETAVATAR, (WPARAM)&avii, 0); pavi->ObjectSize = avii.szSize; return MAKELONG(avii.szSize.cx, avii.szSize.cy); } ///////////////////////////////////////////////////////////////////////////////////////// // reset positions of avatars to be drawn (still be painted at same place) int AniAva_InvalidateAvatarPositions(MCONTACT hContact) { aacheck 0; mir_cslock lck(s_CS); if (hContact) { ANIAVA_OBJECT *pai = FindAvatarByContact(hContact); if (pai) pai->bInvalidPos++; } else for (auto &it : s_Objects) it->bInvalidPos++; return 1; } ///////////////////////////////////////////////////////////////////////////////////////// // repaint all avatars at positions (eg on main window movement) int AniAva_RedrawAllAvatars(BOOL updateZOrder) { aacheck 0; mir_cslock lck(s_CS); for (auto &it : s_Objects) { if (updateZOrder) SendMessage(it->hWindow, AAM_REDRAW, (WPARAM)updateZOrder, 0); else SendNotifyMessage(it->hWindow, AAM_REDRAW, (WPARAM)updateZOrder, 0); } return 1; } ///////////////////////////////////////////////////////////////////////////////////////// // remove avatar int AniAva_RemoveAvatar(MCONTACT hContact) { aacheck 0; mir_cslock lck(s_CS); ANIAVA_OBJECT *pai = FindAvatarByContact(hContact); if (pai) s_Objects.remove(pai); return 1; } ///////////////////////////////////////////////////////////////////////////////////////// // all avatars without validated position will be stop painted and probably removed int AniAva_RemoveInvalidatedAvatars() { aacheck 0; mir_cslock lck(s_CS); for (auto &it : s_Objects) if (it->hWindow && it->bInvalidPos) { SendMessage(it->hWindow, AAM_STOP, 0, 0); it->bInvalidPos = 0; DestroyWindow(it->hWindow); it->hWindow = nullptr; } return 1; } ///////////////////////////////////////////////////////////////////////////////////////// int AniAva_RenderAvatar(MCONTACT hContact, HDC hdcMem, RECT *rc) { aacheck 0; mir_cslock lck(s_CS); ANIAVA_OBJECT *pai = FindAvatarByContact(hContact); if (pai) SendMessage(pai->hWindow, AAM_RENDER, (WPARAM)hdcMem, (LPARAM)rc); return 0; } ///////////////////////////////////////////////////////////////////////////////////////// // update avatars pos int AniAva_SetAvatarPos(MCONTACT hContact, RECT *rc, int overlayIdx, uint8_t bAlpha) { aacheck 0; mir_cslock lck(s_CS); ANIAVA_OBJECT *pai = FindAvatarByContact(hContact); if (pai == nullptr) return 1; ANIAVA_POSINFO *api = (ANIAVA_POSINFO *)malloc(sizeof(ANIAVA_POSINFO)); if (!pai->hWindow) { // not found -> create window char szName[150] = "AniAvaWnd_"; _itoa((int)hContact, szName + 10, 16); HWND hwnd = _AniAva_CreateAvatarWindowSync(_A2T(szName)); HWND parent = GetAncestor(g_clistApi.hwndContactList, GA_PARENT); pai->hWindow = hwnd; SendMessage(hwnd, AAM_SETPARENT, (WPARAM)parent, 0); ANIAVATARIMAGEINFO avii = { 0 }; if (_AniAva_GetAvatarImageInfo(pai->dwAvatarUniqId, &avii)) SendMessage(pai->hWindow, AAM_SETAVATAR, (WPARAM)&avii, 0); } api->bAlpha = bAlpha; api->idxOverlay = overlayIdx; api->rcPos = *rc; SendNotifyMessage(pai->hWindow, AAM_SETPOSITION, 0, (LPARAM)api); // the AAM_SETPOSITION is responsible to destroy memory under api pai->bInvalidPos = FALSE; return 1; } ///////////////////////////////////////////////////////////////////////////////////////// // Update options int AniAva_UpdateOptions() { BOOL bReloadAvatars = FALSE; BOOL bBeEnabled = g_plugin.getByte("AvatarsAnimated", g_plugin.getByte("AvatarsShow", SETTINGS_SHOWAVATARS_DEFAULT)); if (bBeEnabled && !s_bModuleStarted) { AniAva_InitModule(); bReloadAvatars = TRUE; } else if (!bBeEnabled && s_bModuleStarted) { AniAva_UnloadModule(); bReloadAvatars = TRUE; } BOOL oldSeparate = s_bSeparateWindow; _AniAva_LoadOptions(); if (oldSeparate != s_bSeparateWindow) { AniAva_InvalidateAvatarPositions(0); AniAva_RemoveInvalidatedAvatars(); } if (bReloadAvatars) PostMessage(g_clistApi.hwndContactTree, INTM_AVATARCHANGED, 0, 0); else AniAva_RedrawAllAvatars(TRUE); return 0; } ///////////////////////////////////////////////////////////////////////////////////////// // call windows to set they parent in order to ensure valid zorder void AniAva_UpdateParent() { aacheck; mir_cslock lck(s_CS); HWND parent = GetAncestor(g_clistApi.hwndContactList, GA_PARENT); for (auto &it : s_Objects) SendMessage(it->hWindow, AAM_SETPARENT, (WPARAM)parent, 0); } ///////////////////////////////////////////////////////////////////////////////////////// // Init AniAva module int AniAva_InitModule() { if (!g_plugin.getByte("AvatarsAnimated", g_plugin.getByte("AvatarsShow", SETTINGS_SHOWAVATARS_DEFAULT))) return 0; WNDCLASSEX wc = { sizeof(wc) }; wc.lpszClassName = ANIAVAWINDOWCLASS; wc.lpfnWndProc = _AniAva_WndProc; wc.hCursor = LoadCursor(nullptr, IDC_ARROW); wc.cbWndExtra = sizeof(ANIAVA_WINDOWINFO*); wc.style = CS_GLOBALCLASS; RegisterClassEx(&wc); s_bModuleStarted = TRUE; _AniAva_LoadOptions(); s_hExitEvent = CreateEvent(nullptr, TRUE, FALSE, nullptr); mir_forkthread(_AniAva_AnimationTreadProc); return 1; } ///////////////////////////////////////////////////////////////////////////////////////// // Unload AniAva module void _AniAva_OnModulesUnload() { if (s_hExitEvent) SetEvent(s_hExitEvent); } int AniAva_UnloadModule() { aacheck 0; mir_cslock lck(s_CS); s_bModuleStarted = FALSE; s_Objects.destroy(); for (auto &aai : s_AniAvatarList) { mir_free(aai->tcsFilename); free(aai->pFrameDelays); mir_free(aai); } s_AniAvatarList.destroy(); _AniAva_RemoveAniAvaDC(); return 1; }