#include "stdafx.h" #ifndef MONITOR_DEFAULTTONULL #define MONITOR_DEFAULTTONULL 0x00000000 #endif // NotesData DB data params #define DATATAG_TEXT 1 // %s #define DATATAG_SCROLLPOS 2 // %u (specifies rich edit controls scroll post as first visible line) #define DATATAG_BGCOL 3 // %x (custom background color) #define DATATAG_FGCOL 4 // %x (custom text/fg colors) #define DATATAG_TITLE 5 // %s (custom note title) #define DATATAG_FONT 6 // %d:%u:%u:%s (custom font) #define MAX_TITLE_LEN 63 #define MAX_NOTE_LEN 16384 // delay before saving note changes (ms) #define NOTE_CHANGE_COMMIT_DELAY 1000 #ifndef WS_EX_NOACTIVATE #define WS_EX_NOACTIVATE 0x08000000 #endif #define WS_EX_LAYERED 0x00080000 #define LWA_ALPHA 0x00000002 #define NOTE_WND_CLASS L"MIM_StickyNote" #define IDM_COLORPRESET_BG 41000 #define IDM_COLORPRESET_FG 41100 struct ColorPreset { wchar_t *szName; COLORREF color; }; static struct ColorPreset clrPresets[] = { { LPGENW("Black"), RGB(0,0,0) }, { LPGENW("Maroon"), RGB(128,0,0) }, { LPGENW("Green"), RGB(0,128,0) }, { LPGENW("Olive"), RGB(128,128,0) }, { LPGENW("Navy"), RGB(0,0,128) }, { LPGENW("Purple"), RGB(128,0,128) }, { LPGENW("Teal"), RGB(0,128,128) }, { LPGENW("Gray"), RGB(128,128,128) }, { LPGENW("Silver"), RGB(192,192,192) }, { LPGENW("Red"), RGB(255,0,0) }, { LPGENW("Orange"), RGB(255,155,0) }, { LPGENW("Lime"), RGB(0,255,0) }, { LPGENW("Yellow"), RGB(255,255,0) }, { LPGENW("Blue"), RGB(0,0,255) }, { LPGENW("Fuchsia"), RGB(255,0,255) }, { LPGENW("Aqua"), RGB(0,255,255) }, { LPGENW("White"), RGB(255,255,255) } }; ///////////////////////////////////////////////////////////////////////////////////////// struct STICKYNOTEFONT : public MZeroedObject { HFONT hFont; char size; uint8_t style; // see the DBFONTF_* flags uint8_t charset; wchar_t szFace[LF_FACESIZE]; }; struct STICKYNOTE : public MZeroedObject { HWND SNHwnd, REHwnd; BOOL bVisible, bOnTop; CMStringW wszText; ULONGLONG ID; // FILETIME in UTC wchar_t *pwszTitle; BOOL CustomTitle; uint32_t BgColor; // custom bg color override (only valid if non-zero) uint32_t FgColor; // custom fg/text color override (only valid if non-zero) STICKYNOTEFONT *pCustomFont;// custom (body) font override (NULL if default font is used) ~STICKYNOTE() { if (SNHwnd) DestroyWindow(SNHwnd); mir_free(pwszTitle); if (pCustomFont) { DeleteObject(pCustomFont->hFont); free(pCustomFont); } } }; static OBJLIST g_arStickies(1, PtrKeySortT); void GetTriggerTimeString(const ULONGLONG *When, wchar_t *s, size_t strSize, BOOL bUtc); void FileTimeToTzLocalST(const FILETIME *lpUtc, SYSTEMTIME *tmLocal); static void NotifyList(); static class CNotesListDlg *pListDialog; COLORREF GetCaptionColor(COLORREF bodyClr) { const uint32_t r = ((bodyClr & 0xff) * 4) / 5; const uint32_t g = (((bodyClr & 0xff00) * 4) / 5) & 0xff00; const uint32_t b = (((bodyClr & 0xff0000) * 4) / 5) & 0xff0000; return (COLORREF)(r | g | b); } static void EnsureUniqueID(STICKYNOTE *TSN) { if (!g_arStickies.getCount()) return; try_next: // check existing notes if id is in use for (auto &it : g_arStickies) { if (it->ID == TSN->ID) { // id in use, try new (increases the ID/time stamp by 100 nanosecond steps until an unused time is found, // allthough it's very unlikely that there will be duplicated id's it's better to make 100% sure) TSN->ID++; goto try_next; } } } static void InitNoteTitle(STICKYNOTE *TSN) { if (g_NoteTitleDate) { wchar_t TempStr[MAX_PATH]; SYSTEMTIME tm; LCID lc = GetUserDefaultLCID(); TempStr[0] = 0; memset(&tm, 0, sizeof(tm)); FileTimeToTzLocalST((FILETIME*)&TSN->ID, &tm); if (GetDateFormatW(lc, 0, &tm, GetDateFormatStr(), TempStr, MAX_PATH)) { // append time if requested if (g_NoteTitleTime) { int n = (int)mir_wstrlen(TempStr); TempStr[n++] = ' '; TempStr[n] = 0; GetTimeFormat(MAKELCID(MAKELANGID(LANG_ENGLISH, SUBLANG_ENGLISH_US), 0), 0, &tm, GetTimeFormatStr(), TempStr + n, MAX_PATH - n); } TSN->pwszTitle = mir_wstrdup(TempStr); } } TSN->CustomTitle = FALSE; } static void InitStickyNoteLogFont(STICKYNOTEFONT *pCustomFont, LOGFONT *lf) { if (!pCustomFont->size) { SystemParametersInfoW(SPI_GETICONTITLELOGFONT, sizeof(*lf), &lf, FALSE); lf->lfHeight = 10; HDC hdc = GetDC(nullptr); lf->lfHeight = -MulDiv(lf->lfHeight, GetDeviceCaps(hdc, LOGPIXELSY), 72); ReleaseDC(nullptr, hdc); } else { lf->lfHeight = pCustomFont->size; } wcsncpy_s(lf->lfFaceName, pCustomFont->szFace, _TRUNCATE); lf->lfWidth = lf->lfEscapement = lf->lfOrientation = 0; lf->lfWeight = pCustomFont->style & DBFONTF_BOLD ? FW_BOLD : FW_NORMAL; lf->lfItalic = (pCustomFont->style & DBFONTF_ITALIC) != 0; lf->lfUnderline = (pCustomFont->style & DBFONTF_UNDERLINE) != 0; lf->lfStrikeOut = (pCustomFont->style & DBFONTF_STRIKEOUT) != 0; lf->lfCharSet = pCustomFont->charset; lf->lfOutPrecision = OUT_DEFAULT_PRECIS; lf->lfClipPrecision = CLIP_DEFAULT_PRECIS; lf->lfQuality = DEFAULT_QUALITY; lf->lfPitchAndFamily = DEFAULT_PITCH | FF_DONTCARE; } static bool CreateStickyNoteFont(STICKYNOTEFONT *pCustomFont, LOGFONT *plf) { LOGFONT lf = {}; if (!plf) { InitStickyNoteLogFont(pCustomFont, &lf); plf = &lf; } if (pCustomFont->hFont) DeleteObject(pCustomFont->hFont); pCustomFont->hFont = CreateFontIndirectW(plf); return pCustomFont->hFont != nullptr; } void PurgeNotes(void) { char ValueName[16]; int NotesCount = g_plugin.getDword("NotesData", 0); for (int i = 0; i < NotesCount; i++) { mir_snprintf(ValueName, "NotesData%d", i); g_plugin.delSetting(ValueName); } } void DeleteNotes(void) { PurgeNotes(); g_plugin.setDword("NotesData", 0); g_arStickies.destroy(); NotifyList(); } void BringAllNotesToFront(STICKYNOTE *pActive) { if (!g_arStickies.getCount()) return; // NOTE: for some reason there are issues when bringing to top through hotkey while another app (like Explorer) // is active, it refuses to move notes to top like it should with HWND_TOP. as a workaround still doesn't // work 100% of the time, but at least more often, we first move not to top-most then for non-always-on-top // notes we demote them back as a non top-most window for (auto &SN : g_arStickies) { if (SN->bVisible && pActive != SN) { SetWindowPos(SN->SNHwnd, HWND_TOPMOST, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE); if (!SN->bOnTop) SetWindowPos(SN->SNHwnd, HWND_NOTOPMOST, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE); } } if (pActive) { SetWindowPos(pActive->SNHwnd, HWND_TOPMOST, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE); if (!pActive->bOnTop) SetWindowPos(pActive->SNHwnd, HWND_NOTOPMOST, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE); } } // pModified optionally points to the modified note that invoked the JustSaveNotes call static void JustSaveNotes(STICKYNOTE *pModified = nullptr) { int i = 0, NotesCount = g_arStickies.getCount(); char ValueName[32]; const int OldNotesCount = g_plugin.getDword("NotesData", 0); g_plugin.setDword("NotesData", NotesCount); for (auto &pNote : g_arStickies) { // window pos and size WINDOWPLACEMENT wp; wp.length = sizeof(WINDOWPLACEMENT); GetWindowPlacement(pNote->SNHwnd, &wp); int TX = wp.rcNormalPosition.left; int TY = wp.rcNormalPosition.top; int TW = wp.rcNormalPosition.right - wp.rcNormalPosition.left; int TH = wp.rcNormalPosition.bottom - wp.rcNormalPosition.top; // set flags uint32_t flags = 0; if (pNote->bVisible) flags |= 1; if (pNote->bOnTop) flags |= 2; // update the data of the modified note if (pNote == pModified) { int SzT = GetWindowTextLengthW(pNote->REHwnd); if (SzT) { // TODO: change to support unicode and rtf, use EM_STREAMOUT if (SzT > MAX_NOTE_LEN) SzT = MAX_NOTE_LEN; // we want to be far below the 64k limit pNote->wszText.Truncate(SzT + 1); GetWindowTextW(pNote->REHwnd, pNote->wszText.GetBuffer(), SzT + 1); } else pNote->wszText.Empty(); } // data header CMStringA szValue; szValue.AppendFormat("X%I64x:%d:%d:%d:%d:%x", pNote->ID, TX, TY, TW, TH, flags); // scroll pos int scrollV = (pNote->wszText.IsEmpty()) ? 0 : SendMessage(pNote->REHwnd, EM_GETFIRSTVISIBLELINE, 0, 0); if (scrollV > 0) szValue.AppendFormat("\033""%u:%u", DATATAG_SCROLLPOS, (UINT)scrollV); // custom bg color if (pNote->BgColor) szValue.AppendFormat("\033""%u:%x", DATATAG_BGCOL, (UINT)(pNote->BgColor & 0xffffff)); // custom fg color if (pNote->FgColor) szValue.AppendFormat("\033""%u:%x", DATATAG_FGCOL, (UINT)(pNote->FgColor & 0xffffff)); if (pNote->pCustomFont) { szValue.AppendFormat("\033""%u:%d:%u:%u:%s", DATATAG_FONT, (int)pNote->pCustomFont->size, (UINT)pNote->pCustomFont->style, (UINT)pNote->pCustomFont->charset, pNote->pCustomFont->szFace); } // custom title if (pNote->CustomTitle && pNote->pwszTitle) szValue.AppendFormat("\033""%u:%s", DATATAG_TITLE, pNote->pwszTitle); // note text (ALWAYS PUT THIS PARAM LAST) if (!pNote->wszText.IsEmpty()) szValue.AppendFormat("\033""%u:%s", DATATAG_TEXT, ptrA(mir_utf8encodeW(pNote->wszText)).get()); mir_snprintf(ValueName, "NotesData%d", i++); // we do not reverse notes in DB db_set_blob(0, MODULENAME, ValueName, szValue.GetBuffer(), szValue.GetLength() + 1); // make no save is queued for the note if (pNote->SNHwnd) KillTimer(pNote->SNHwnd, 1025); } // delete any left over DB note entries for (; i < OldNotesCount; i++) { mir_snprintf(ValueName, "NotesData%d", i); g_plugin.delSetting(ValueName); } NotifyList(); } void OnDeleteNote(HWND hdlg, STICKYNOTE *SN) { if (MessageBoxW(hdlg, TranslateT("Are you sure you want to delete this note?"), TranslateT(SECTIONNAME), MB_OKCANCEL) == IDOK) { g_arStickies.remove(SN); JustSaveNotes(); NotifyList(); } } void ShowHideNotes(void) { if (!g_arStickies.getCount()) return; // if some notes are hidden but others visible then first make all visible // only toggle vis state if all are hidden or all are visible UINT nHideCount = 0, nVisCount = 0; for (auto &SN : g_arStickies) { if (SN->bVisible) nVisCount++; else nHideCount++; } bool bVisible; if (!nVisCount) bVisible = true; else if (!nHideCount) bVisible = false; else bVisible = true; int bShow = bVisible ? SW_SHOWNA : SW_HIDE; for (auto &SN : g_arStickies) { if ((!bVisible) != (!SN->bVisible)) { ShowWindow(SN->SNHwnd, bShow); SN->bVisible = bVisible; } } JustSaveNotes(); } void SaveNotes(void) { JustSaveNotes(); g_arStickies.destroy(); } ///////////////////////////////////////////////////////////////////////////////////////// // Note Window static int FindMenuItem(HMENU h, LPSTR lpszName) { int n = GetMenuItemCount(h); if (n <= 0) return -1; // searches for a menu item based on name (used to avoid hardcoding item indices for sub-menus) for (int i = 0; i < n; i++) { char s[128]; if (GetMenuStringA(h, i, s, 128, MF_BYPOSITION)) if (!mir_strcmp(s, lpszName)) return (int)i; } return -1; } static BOOL DoContextMenu(HWND m_hwnd, WPARAM, LPARAM lParam) { STICKYNOTE *SN = (STICKYNOTE*)GetPropA(m_hwnd, "ctrldata"); HMENU hMenuLoad, FhMenu, hSub; hMenuLoad = LoadMenuA(g_plugin.getInst(), "MNU_NOTEPOPUP"); FhMenu = GetSubMenu(hMenuLoad, 0); if (SN->bOnTop) CheckMenuItem(FhMenu, ID_CONTEXTMENUNOTE_TOGGLEONTOP, MF_CHECKED | MF_BYCOMMAND); EnableMenuItem(FhMenu, ID_CONTEXTMENUNOTE_PASTETITLE, MF_BYCOMMAND | (IsClipboardFormatAvailable(CF_TEXT) ? MF_ENABLED : MF_GRAYED)); if (!SN->CustomTitle) EnableMenuItem(FhMenu, ID_CONTEXTMENUNOTE_RESETTITLE, MF_BYCOMMAND | MF_GRAYED); // NOTE: names used for FindMenuItem would need to include & chars if such shortcuts are added to the menus int n = FindMenuItem(FhMenu, "Appearance"); if (n >= 0 && (hSub = GetSubMenu(FhMenu, n))) { HMENU hBg = GetSubMenu(hSub, FindMenuItem(hSub, "Background Color")); HMENU hFg = GetSubMenu(hSub, FindMenuItem(hSub, "Text Color")); for (int i = 0; i < _countof(clrPresets); i++) InsertMenu(hBg, i, MF_BYPOSITION | MF_OWNERDRAW, IDM_COLORPRESET_BG + i, TranslateW(clrPresets[i].szName)); for (int i = 0; i < _countof(clrPresets); i++) InsertMenu(hFg, i, MF_BYPOSITION | MF_OWNERDRAW, IDM_COLORPRESET_FG + i, TranslateW(clrPresets[i].szName)); } TranslateMenu(FhMenu); TrackPopupMenu(FhMenu, TPM_LEFTALIGN | TPM_RIGHTBUTTON, LOWORD(lParam), HIWORD(lParam), 0, m_hwnd, nullptr); DestroyMenu(hMenuLoad); return TRUE; } static void MeasureColorPresetMenuItem(HWND hdlg, LPMEASUREITEMSTRUCT lpMeasureItem, struct ColorPreset *clrPresets2) { HDC hdc = GetDC(hdlg); wchar_t *lpsz = TranslateW(clrPresets2->szName); SIZE sz; GetTextExtentPoint32(hdc, lpsz, (int)mir_wstrlen(lpsz), &sz); ReleaseDC(hdlg, hdc); lpMeasureItem->itemWidth = 50 + sz.cx; lpMeasureItem->itemHeight = (sz.cy + 2) > 18 ? sz.cy + 2 : 18; } static void PaintColorPresetMenuItem(LPDRAWITEMSTRUCT lpDrawItem, struct ColorPreset *clrPresets2) { // UINT n = lpDrawItem->itemID - IDM_COLORPRESET_BG; RECT rect; rect.left = lpDrawItem->rcItem.left + 50; rect.top = lpDrawItem->rcItem.top; rect.right = lpDrawItem->rcItem.right; rect.bottom = lpDrawItem->rcItem.bottom; if (lpDrawItem->itemState & ODS_SELECTED) { SetDCBrushColor(lpDrawItem->hDC, GetSysColor(COLOR_MENUHILIGHT)); FillRect(lpDrawItem->hDC, &lpDrawItem->rcItem, (HBRUSH)GetStockObject(DC_BRUSH)); SetTextColor(lpDrawItem->hDC, GetSysColor(COLOR_HIGHLIGHTTEXT)); } else { SetDCBrushColor(lpDrawItem->hDC, GetSysColor(COLOR_MENU)); FillRect(lpDrawItem->hDC, &lpDrawItem->rcItem, (HBRUSH)GetStockObject(DC_BRUSH)); SetTextColor(lpDrawItem->hDC, GetSysColor(COLOR_MENUTEXT)); } SetBkMode(lpDrawItem->hDC, TRANSPARENT); DrawText(lpDrawItem->hDC, clrPresets2->szName, -1, &rect, DT_LEFT | DT_SINGLELINE | DT_END_ELLIPSIS | DT_VCENTER); int h = lpDrawItem->rcItem.bottom - lpDrawItem->rcItem.top; rect.left = lpDrawItem->rcItem.left + 5; rect.top = lpDrawItem->rcItem.top + ((h - 14) >> 1); rect.right = rect.left + 40; rect.bottom = rect.top + 14; FrameRect(lpDrawItem->hDC, &rect, (HBRUSH)GetStockObject(BLACK_BRUSH)); rect.left++; rect.top++; rect.right--; rect.bottom--; SetDCBrushColor(lpDrawItem->hDC, clrPresets2->color); FillRect(lpDrawItem->hDC, &rect, (HBRUSH)GetStockObject(DC_BRUSH)); } static BOOL GetClipboardText_Title(wchar_t *pOut, int size) { BOOL bResult = FALSE; if (OpenClipboard(nullptr)) { HANDLE hData = GetClipboardData(CF_UNICODETEXT); wchar_t *buffer; if (hData && (buffer = (wchar_t*)GlobalLock(hData))) { // trim initial white spaces while (*buffer && iswspace(*buffer)) buffer++; size_t n = mir_wstrlen(buffer); if (n >= size) n = size - 1; wcsncpy_s(pOut, size, buffer, _TRUNCATE); // end string on line break and convert tabs to spaces wchar_t *p = pOut; while (*p) { if (*p == '\r' || *p == '\n') { *p = 0; n = mir_wstrlen(pOut); break; } else if (*p == '\t') { *p = ' '; } p++; } // trim trailing white spaces rtrimw(pOut); if (pOut[0]) bResult = TRUE; GlobalUnlock(hData); } CloseClipboard(); } return bResult; } static void SetNoteTextControl(STICKYNOTE *SN) { CHARFORMAT CF = {}; CF.cbSize = sizeof(CHARFORMAT); CF.dwMask = CFM_COLOR; CF.crTextColor = SN->FgColor ? (SN->FgColor & 0xffffff) : BodyFontColor; SendMessage(SN->REHwnd, EM_SETCHARFORMAT, SCF_ALL, (LPARAM)&CF); SetWindowTextW(SN->REHwnd, SN->wszText); } static UINT_PTR CALLBACK CFHookProc(HWND hdlg, UINT msg, WPARAM, LPARAM) { if (msg == WM_INITDIALOG) { // hide color selector ShowWindow(GetDlgItem(hdlg, 0x443), SW_HIDE); ShowWindow(GetDlgItem(hdlg, 0x473), SW_HIDE); TranslateDialogDefault(hdlg); } return 0; } LRESULT CALLBACK StickyNoteWndProc(HWND hdlg, UINT message, WPARAM wParam, LPARAM lParam) { STICKYNOTE *SN = (STICKYNOTE*)GetPropA(hdlg, "ctrldata"); switch (message) { case WM_CLOSE: return TRUE; case WM_SIZE: RECT SZ; GetClientRect(hdlg, &SZ); MoveWindow(GetDlgItem(hdlg, 1), 0, 0, SZ.right, SZ.bottom, TRUE); KillTimer(hdlg, 1025); SetTimer(hdlg, 1025, NOTE_CHANGE_COMMIT_DELAY, nullptr); return TRUE; case WM_TIMER: if (wParam == 1025) { KillTimer(hdlg, 1025); JustSaveNotes(SN); } break; case WM_MOVE: KillTimer(hdlg, 1025); SetTimer(hdlg, 1025, NOTE_CHANGE_COMMIT_DELAY, nullptr); return TRUE; case WM_CREATE: { CREATESTRUCT *CS = (CREATESTRUCT *)lParam; uint32_t mystyle; SN = (STICKYNOTE*)CS->lpCreateParams; SetPropA(hdlg, "ctrldata", (HANDLE)SN); BringWindowToTop(hdlg); mystyle = WS_CHILD | WS_VISIBLE | ES_LEFT | ES_MULTILINE | ES_AUTOVSCROLL | ES_WANTRETURN; if (g_plugin.bShowScrollbar) mystyle |= WS_VSCROLL; HWND H = CreateWindowW(MSFTEDIT_CLASS, nullptr, mystyle, 0, 0, CS->cx - 3 - 3, CS->cy - 3 - (3 + 14), hdlg, (HMENU)1, hmiranda, nullptr); SN->REHwnd = H; SendMessage(H, EM_SETTEXTMODE, TM_PLAINTEXT, 0); SendMessage(H, EM_LIMITTEXT, MAX_NOTE_LEN, 0); SendMessage(H, WM_SETFONT, (WPARAM)(SN->pCustomFont ? SN->pCustomFont->hFont : hBodyFont), 1); SendMessage(H, EM_SETEVENTMASK, 0, ENM_CHANGE | ENM_LINK); SendMessage(H, EM_SETBKGNDCOLOR, 0, SN->BgColor ? (SN->BgColor & 0xffffff) : BodyColor); SendMessage(H, EM_AUTOURLDETECT, 1, 0); SetNoteTextControl(SN); } return TRUE; case WM_GETMINMAXINFO: { MINMAXINFO *mm = (MINMAXINFO*)lParam; // min width accomodates frame, buttons and some extra space for sanity mm->ptMinTrackSize.x = 48 + 3 + 3 + 8 + 40; // min height allows collapsing entire client area, only leaving frame and caption mm->ptMinTrackSize.y = 3 + 3 + 14; } return 0; case WM_ERASEBKGND: // no BG needed as edit control takes up entire client area return TRUE; case WM_NCPAINT: // make window borders have the same color as caption { RECT rect, wr, r; HDC hdc = GetWindowDC(hdlg); GetWindowRect(hdlg, &wr); if (wParam && wParam != 1) { SelectClipRgn(hdc, (HRGN)wParam); OffsetClipRgn(hdc, -wr.left, -wr.top); } rect = wr; OffsetRect(&rect, -wr.left, -wr.top); HBRUSH hBkBrush = (HBRUSH)GetStockObject(DC_BRUSH); SetDCBrushColor(hdc, GetCaptionColor((SN && SN->BgColor) ? SN->BgColor : BodyColor)); // draw all frame sides separately to avoid filling client area (which flickers) { // top r.left = rect.left; r.right = rect.right; r.top = rect.top; r.bottom = r.top + 3 + 14; FillRect(hdc, &r, hBkBrush); // bottom r.top = rect.bottom - 3; r.bottom = rect.bottom; FillRect(hdc, &r, hBkBrush); // left r.left = rect.left; r.right = r.left + 3; r.top = rect.top + 3 + 14; r.bottom = rect.bottom - 3; FillRect(hdc, &r, hBkBrush); // right r.left = rect.right - 3; r.right = rect.right; FillRect(hdc, &r, hBkBrush); } // paint title bar contents (time stamp and buttons) if (SN && SN->pwszTitle) { RECT R; SelectObject(hdc, hCaptionFont); R.top = 3 + 1; R.bottom = 3 + 11; R.left = 3 + 2; R.right = rect.right - 3 - 1; if (g_plugin.bShowNoteButtons) R.right -= 48; SetTextColor(hdc, SN->FgColor ? (SN->FgColor & 0xffffff) : CaptionFontColor); SetBkMode(hdc, TRANSPARENT); DrawTextW(hdc, SN->pwszTitle, -1, &R, DT_LEFT | DT_SINGLELINE | DT_END_ELLIPSIS | DT_VCENTER); } if (g_plugin.bShowNoteButtons) { HICON hcIcon; if (SN->bOnTop) hcIcon = IcoLib_GetIconByHandle(iconList[4].hIcolib); else hcIcon = IcoLib_GetIconByHandle(iconList[7].hIcolib); DrawIcon(hdc, wr.right - wr.left - 16, 0 + 3, hcIcon); IcoLib_ReleaseIcon(hcIcon); hcIcon = IcoLib_GetIconByHandle(iconList[9].hIcolib); DrawIcon(hdc, wr.right - wr.left - 32, 1 + 3, hcIcon); IcoLib_ReleaseIcon(hcIcon); hcIcon = IcoLib_GetIconByHandle(iconList[8].hIcolib); DrawIcon(hdc, wr.right - wr.left - 48, 1 + 3, hcIcon); IcoLib_ReleaseIcon(hcIcon); } if (wParam && wParam != 1) SelectClipRgn(hdc, nullptr); ReleaseDC(hdlg, hdc); } return TRUE; case WM_NCCALCSIZE: { RECT *pRect = wParam ? &((NCCALCSIZE_PARAMS*)lParam)->rgrc[0] : (RECT*)lParam; pRect->bottom -= 3; pRect->right -= 3; pRect->left += 3; pRect->top += 3 + 14; } return WVR_REDRAW; case WM_NCACTIVATE: // update window (so that parts that potentially became visible through activation get redrawn immediately) RedrawWindow(hdlg, nullptr, nullptr, RDW_UPDATENOW); return TRUE; case WM_NOTIFY: if (LOWORD(wParam) == 1) { ENLINK *PEnLnk = (ENLINK*)lParam; if (PEnLnk->msg == WM_LBUTTONDOWN) { SendDlgItemMessage(hdlg, 1, EM_EXSETSEL, 0, (LPARAM)&(PEnLnk->chrg)); char* Buff = (char*)malloc(PEnLnk->chrg.cpMax - PEnLnk->chrg.cpMin + 1); SendDlgItemMessage(hdlg, 1, EM_GETSELTEXT, 0, (LPARAM)Buff); if ((GetAsyncKeyState(VK_CONTROL) >> 15) != 0) ShellExecuteA(hdlg, "open", "iexplore", Buff, "", SW_SHOWNORMAL); else if (g_lpszAltBrowser && *g_lpszAltBrowser) ShellExecuteA(hdlg, "open", g_lpszAltBrowser, Buff, "", SW_SHOWNORMAL); else ShellExecuteA(hdlg, "open", Buff, "", "", SW_SHOWNORMAL); free(Buff); return TRUE; } return FALSE; } break; case WM_NCHITTEST: { int r = DefWindowProc(hdlg, message, wParam, lParam); // filter out potential hits on windows default title bar buttons switch (r) { case HTSYSMENU: case HTCLOSE: case HTMINBUTTON: case HTMAXBUTTON: return HTCAPTION; } return r; } case WM_NCLBUTTONDOWN: if (wParam == HTCAPTION && g_plugin.bShowNoteButtons) { long X, Y; RECT rect; int Tw; GetWindowRect(hdlg, &rect); Tw = rect.right - rect.left; X = LOWORD(lParam) - rect.left; Y = HIWORD(lParam) - rect.top; if (X > Tw - 16) { SendMessage(hdlg, WM_COMMAND, ID_CONTEXTMENUNOTE_TOGGLEONTOP, 0); return TRUE; } else if (X > Tw - 31 && X < Tw - 16) { SendMessage(hdlg, WM_COMMAND, ID_CONTEXTMENUNOTE_REMOVENOTE, 0); return TRUE; } else if (X > Tw - 48 && X < Tw - 32) { SendMessage(hdlg, WM_COMMAND, ID_CONTEXTMENUNOTE_HIDENOTE, 0); return TRUE; } } return DefWindowProc(hdlg, message, wParam, lParam); case WM_MEASUREITEM: { LPMEASUREITEMSTRUCT lpMeasureItem = (LPMEASUREITEMSTRUCT)lParam; if (lpMeasureItem->CtlType != ODT_MENU) break; if (lpMeasureItem->itemID >= IDM_COLORPRESET_BG && lpMeasureItem->itemID <= IDM_COLORPRESET_BG + _countof(clrPresets)) { MeasureColorPresetMenuItem(hdlg, lpMeasureItem, clrPresets + (lpMeasureItem->itemID - IDM_COLORPRESET_BG)); return TRUE; } else if (lpMeasureItem->itemID >= IDM_COLORPRESET_FG && lpMeasureItem->itemID <= IDM_COLORPRESET_FG + _countof(clrPresets)) { MeasureColorPresetMenuItem(hdlg, lpMeasureItem, clrPresets + (lpMeasureItem->itemID - IDM_COLORPRESET_FG)); return TRUE; } } break; case WM_DRAWITEM: if (!wParam) { LPDRAWITEMSTRUCT lpDrawItem = (LPDRAWITEMSTRUCT)lParam; if (lpDrawItem->CtlType != ODT_MENU) break; if (lpDrawItem->itemID >= IDM_COLORPRESET_BG && lpDrawItem->itemID <= IDM_COLORPRESET_BG + _countof(clrPresets)) { PaintColorPresetMenuItem(lpDrawItem, clrPresets + (lpDrawItem->itemID - IDM_COLORPRESET_BG)); return TRUE; } else if (lpDrawItem->itemID >= IDM_COLORPRESET_FG && lpDrawItem->itemID <= IDM_COLORPRESET_FG + _countof(clrPresets)) { PaintColorPresetMenuItem(lpDrawItem, clrPresets + (lpDrawItem->itemID - IDM_COLORPRESET_FG)); return TRUE; } } break; case WM_COMMAND: UINT id; switch (HIWORD(wParam)) { case EN_CHANGE: case EN_VSCROLL: case EN_HSCROLL: KillTimer(hdlg, 1025); SetTimer(hdlg, 1025, NOTE_CHANGE_COMMIT_DELAY, nullptr); break; } id = (UINT)LOWORD(wParam); if (id >= IDM_COLORPRESET_BG && id <= IDM_COLORPRESET_BG + _countof(clrPresets)) { SN->BgColor = clrPresets[id - IDM_COLORPRESET_BG].color | 0xff000000; SendMessage(SN->REHwnd, EM_SETBKGNDCOLOR, 0, SN->BgColor & 0xffffff); RedrawWindow(SN->SNHwnd, nullptr, nullptr, RDW_INVALIDATE | RDW_FRAME | RDW_UPDATENOW); JustSaveNotes(); return FALSE; } if (id >= IDM_COLORPRESET_FG && id <= IDM_COLORPRESET_FG + _countof(clrPresets)) { SN->FgColor = clrPresets[id - IDM_COLORPRESET_FG].color | 0xff000000; CHARFORMAT CF = {}; CF.cbSize = sizeof(CHARFORMAT); CF.dwMask = CFM_COLOR; CF.crTextColor = SN->FgColor & 0xffffff; SendMessage(SN->REHwnd, EM_SETCHARFORMAT, SCF_ALL, (LPARAM)&CF); RedrawWindow(SN->SNHwnd, nullptr, nullptr, RDW_INVALIDATE | RDW_FRAME | RDW_UPDATENOW); JustSaveNotes(); return FALSE; } switch (id) { case ID_CONTEXTMENUNOTE_NEWNOTE: PluginMenuCommandAddNew(0, 0); break; case ID_BACKGROUNDCOLOR_CUSTOM: { COLORREF custclr[16] = {0}; CHOOSECOLOR cc = {0}; COLORREF orgclr = SN->BgColor ? (COLORREF)(SN->BgColor & 0xffffff) : (COLORREF)(BodyColor & 0xffffff); cc.lStructSize = sizeof(cc); cc.hwndOwner = SN->SNHwnd; cc.rgbResult = orgclr; cc.Flags = CC_ANYCOLOR | CC_FULLOPEN | CC_RGBINIT | CC_SOLIDCOLOR; cc.lpCustColors = custclr; if (ChooseColor(&cc) && cc.rgbResult != orgclr) { SN->BgColor = cc.rgbResult | 0xff000000; SendMessage(SN->REHwnd, EM_SETBKGNDCOLOR, 0, SN->BgColor & 0xffffff); RedrawWindow(SN->SNHwnd, nullptr, nullptr, RDW_INVALIDATE | RDW_FRAME | RDW_UPDATENOW); JustSaveNotes(); } } break; case ID_TEXTCOLOR_CUSTOM: { COLORREF custclr[16] = {0}; COLORREF orgclr = SN->FgColor ? (COLORREF)(SN->FgColor & 0xffffff) : (COLORREF)(BodyFontColor & 0xffffff); CHOOSECOLOR cc = {0}; cc.lStructSize = sizeof(cc); cc.hwndOwner = SN->SNHwnd; cc.rgbResult = orgclr; cc.Flags = CC_ANYCOLOR | CC_FULLOPEN | CC_RGBINIT | CC_SOLIDCOLOR; cc.lpCustColors = custclr; if (ChooseColor(&cc) && cc.rgbResult != orgclr) { SN->FgColor = cc.rgbResult | 0xff000000; CHARFORMAT CF = {0}; CF.cbSize = sizeof(CHARFORMAT); CF.dwMask = CFM_COLOR; CF.crTextColor = SN->FgColor & 0xffffff; SendMessage(SN->REHwnd, EM_SETCHARFORMAT, SCF_ALL, (LPARAM)&CF); RedrawWindow(SN->SNHwnd, nullptr, nullptr, RDW_INVALIDATE | RDW_FRAME | RDW_UPDATENOW); JustSaveNotes(); } } break; case ID_FONT_CUSTOM: { LOGFONT lf = {}; if (SN->pCustomFont) InitStickyNoteLogFont(SN->pCustomFont, &lf); else LoadNRFont(NR_FONTID_BODY, &lf, nullptr); CHOOSEFONT cf = {}; cf.lStructSize = sizeof(cf); cf.hwndOwner = SN->SNHwnd; cf.lpLogFont = &lf; cf.Flags = CF_EFFECTS | CF_FORCEFONTEXIST | CF_INITTOLOGFONTSTRUCT | CF_SCREENFONTS | CF_ENABLEHOOK; cf.lpfnHook = CFHookProc; if (!ChooseFontW(&cf)) break; if (!SN->pCustomFont) { SN->pCustomFont = (STICKYNOTEFONT*)malloc(sizeof(STICKYNOTEFONT)); SN->pCustomFont->hFont = nullptr; } SN->pCustomFont->size = (char)lf.lfHeight; SN->pCustomFont->style = (lf.lfWeight >= FW_BOLD ? DBFONTF_BOLD : 0) | (lf.lfItalic ? DBFONTF_ITALIC : 0) | (lf.lfUnderline ? DBFONTF_UNDERLINE : 0) | (lf.lfStrikeOut ? DBFONTF_STRIKEOUT : 0); SN->pCustomFont->charset = lf.lfCharSet; wcsncpy_s(SN->pCustomFont->szFace, lf.lfFaceName, _TRUNCATE); if (!CreateStickyNoteFont(SN->pCustomFont, &lf)) { // failed free(SN->pCustomFont); SN->pCustomFont = nullptr; } // clear text first to force a reformatting w.r.w scrollbar SetWindowTextA(SN->REHwnd, ""); SendMessage(SN->REHwnd, WM_SETFONT, (WPARAM)(SN->pCustomFont ? SN->pCustomFont->hFont : hBodyFont), FALSE); SetNoteTextControl(SN); RedrawWindow(SN->SNHwnd, nullptr, nullptr, RDW_INVALIDATE | RDW_FRAME | RDW_UPDATENOW); JustSaveNotes(); } break; case ID_BACKGROUNDCOLOR_RESET: SN->BgColor = 0; SendMessage(SN->REHwnd, EM_SETBKGNDCOLOR, 0, (LPARAM)BodyColor); RedrawWindow(SN->SNHwnd, nullptr, nullptr, RDW_INVALIDATE | RDW_FRAME | RDW_UPDATENOW); JustSaveNotes(); break; case ID_TEXTCOLOR_RESET: SN->FgColor = 0; { CHARFORMAT CF = {}; CF.cbSize = sizeof(CHARFORMAT); CF.dwMask = CFM_COLOR; CF.crTextColor = BodyFontColor; SendMessage(SN->REHwnd, EM_SETCHARFORMAT, SCF_ALL, (LPARAM)&CF); } RedrawWindow(SN->SNHwnd, nullptr, nullptr, RDW_INVALIDATE | RDW_FRAME | RDW_UPDATENOW); JustSaveNotes(); break; case ID_FONT_RESET: if (SN->pCustomFont) { DeleteObject(SN->pCustomFont->hFont); free(SN->pCustomFont); SN->pCustomFont = nullptr; // clear text first to force a reformatting w.r.w scrollbar SetWindowTextA(SN->REHwnd, ""); SendMessage(SN->REHwnd, WM_SETFONT, (WPARAM)hBodyFont, FALSE); SetNoteTextControl(SN); RedrawWindow(SN->SNHwnd, nullptr, nullptr, RDW_INVALIDATE | RDW_FRAME | RDW_UPDATENOW); JustSaveNotes(); } break; case ID_CONTEXTMENUNOTE_PASTETITLE: { wchar_t s[MAX_TITLE_LEN + 1]; if (GetClipboardText_Title(s, _countof(s))) { replaceStrW(SN->pwszTitle, s); SN->CustomTitle = true; RedrawWindow(SN->SNHwnd, nullptr, nullptr, RDW_INVALIDATE | RDW_FRAME | RDW_UPDATENOW); JustSaveNotes(); } } break; case ID_CONTEXTMENUNOTE_RESETTITLE: if (SN->CustomTitle) { replaceStrW(SN->pwszTitle, nullptr); InitNoteTitle(SN); RedrawWindow(SN->SNHwnd, nullptr, nullptr, RDW_INVALIDATE | RDW_FRAME | RDW_UPDATENOW); JustSaveNotes(); } break; case ID_CONTEXTMENUNOTE_REMOVENOTE: OnDeleteNote(hdlg, SN); break; case ID_CONTEXTMENUNOTE_HIDENOTE: SN->bVisible = false; ShowWindow(hdlg, SW_HIDE); JustSaveNotes(); break; case ID_CONTEXTMENUNOTE_COPY: SendMessage(SN->REHwnd, WM_COPY, 0, 0); break; case ID_CONTEXTMENUNOTE_PASTE: SendMessage(SN->REHwnd, WM_PASTE, 0, 0); break; case ID_CONTEXTMENUNOTE_CUT: SendMessage(SN->REHwnd, WM_CUT, 0, 0); break; case ID_CONTEXTMENUNOTE_CLEAR: SendMessage(SN->REHwnd, WM_CLEAR, 0, 0); break; case ID_CONTEXTMENUNOTE_UNDO: SendMessage(SN->REHwnd, WM_UNDO, 0, 0); break; case ID_CONTEXTMENUNOTE_TOGGLEONTOP: SN->bOnTop = !SN->bOnTop; SetWindowPos(hdlg, SN->bOnTop ? HWND_TOPMOST : HWND_NOTOPMOST, 0, 0, 0, 0, SWP_SHOWWINDOW | SWP_NOMOVE | SWP_NOSIZE); RedrawWindow(hdlg, nullptr, nullptr, RDW_FRAME | RDW_INVALIDATE | RDW_UPDATENOW); JustSaveNotes(); break; case ID_CONTEXTMENUNOTE_VIEWNOTES: PluginMenuCommandViewNotes(0, 0); break; case ID_CONTEXTMENUNOTE_BRINGALLTOTOP: BringAllNotesToFront(SN); break; } return TRUE; case WM_NCDESTROY: RemovePropA(hdlg, "ctrldata"); break; case WM_CONTEXTMENU: if (DoContextMenu(hdlg, wParam, lParam)) return FALSE; default: return DefWindowProc(hdlg, message, wParam, lParam); } return FALSE; } static STICKYNOTE* NewNoteEx(int Ax, int Ay, int Aw, int Ah, const wchar_t *pwszText, ULONGLONG *ID, BOOL bVisible, BOOL bOnTop, int scrollV, COLORREF bgClr, COLORREF fgClr, wchar_t *Title, STICKYNOTEFONT *pCustomFont, BOOL bLoading) { WNDCLASSEX TWC = {0}; WINDOWPLACEMENT TWP; uint32_t L1, L2; SYSTEMTIME tm; const BOOL bIsStartup = bVisible & 0x10000; bVisible &= ~0x10000; if (!GetClassInfoEx(hmiranda, NOTE_WND_CLASS, &TWC)) { TWC.style = CS_HREDRAW | CS_VREDRAW | CS_NOCLOSE; TWC.cbClsExtra = 0; TWC.cbWndExtra = 0; TWC.hInstance = hmiranda; TWC.hIcon = LoadIcon(nullptr, IDI_APPLICATION); TWC.hCursor = LoadCursor(nullptr, IDC_ARROW); TWC.hbrBackground = nullptr; TWC.lpszMenuName = nullptr; TWC.lpszClassName = NOTE_WND_CLASS; TWC.cbSize = sizeof(WNDCLASSEX); TWC.lpfnWndProc = StickyNoteWndProc; if (!RegisterClassEx(&TWC)) return nullptr; } if (!pwszText || Aw < 0 || Ah < 0) { TWP.length = sizeof(WINDOWPLACEMENT); GetWindowPlacement(GetDesktopWindow(), &TWP); Aw = g_NoteWidth; Ah = g_NoteHeight; Ax = ((TWP.rcNormalPosition.right - TWP.rcNormalPosition.left) / 2) - (Aw / 2); Ay = ((TWP.rcNormalPosition.bottom - TWP.rcNormalPosition.top) / 2) - (Ah / 2); } STICKYNOTE *TSN = new STICKYNOTE(); if (ID) TSN->ID = *ID; else { GetSystemTime(&tm); SystemTimeToFileTime(&tm, (FILETIME*)&TSN->ID); } EnsureUniqueID(TSN); g_arStickies.insert(TSN); if (pwszText) TSN->wszText = pwszText; // init note title (time-stamp) if (Title) { TSN->pwszTitle = Title; TSN->CustomTitle = TRUE; } else { TSN->pwszTitle = nullptr; InitNoteTitle(TSN); } TSN->bVisible = bVisible; TSN->bOnTop = bOnTop; TSN->BgColor = bgClr; TSN->FgColor = fgClr; TSN->pCustomFont = pCustomFont; L1 = WS_EX_TOOLWINDOW; if (g_Transparency < 255) L1 |= WS_EX_LAYERED; if (bOnTop) L1 |= WS_EX_TOPMOST; L2 = WS_POPUP | WS_THICKFRAME | WS_CAPTION; // NOTE: loaded note positions stem from GetWindowPlacement, which normally have a different coord space than // CreateWindow/SetWindowPos, BUT since we now use WS_EX_TOOLWINDOW they use the same coord space so // we don't have to worry about notes "drifting" between sessions TSN->SNHwnd = CreateWindowEx(L1, NOTE_WND_CLASS, L"StickyNote", L2, Ax, Ay, Aw, Ah, nullptr, nullptr, hmiranda, TSN); if (g_Transparency < 255) SetLayeredWindowAttributes(TSN->SNHwnd, 0, (uint8_t)g_Transparency, LWA_ALPHA); // ensure that window is not placed off-screen (if previous session had different monitor count or resolution) // NOTE: SetWindowPlacement should do this, but it's extremly flakey if (pwszText) { if (!MonitorFromWindow(TSN->SNHwnd, MONITOR_DEFAULTTONULL)) { TWP.length = sizeof(WINDOWPLACEMENT); GetWindowPlacement(GetDesktopWindow(), &TWP); if (Aw > 500) Aw = 500; if (Ay < TWP.rcNormalPosition.left + 10 || Ax > TWP.rcNormalPosition.right - 120) Ax = ((TWP.rcNormalPosition.right - TWP.rcNormalPosition.left) / 2) - (Aw / 2) + (rand() & 0x3f); if (Ay < TWP.rcNormalPosition.top + 50 || Ay > TWP.rcNormalPosition.bottom - 50) Ay = ((TWP.rcNormalPosition.bottom - TWP.rcNormalPosition.top) / 4) + (rand() & 0x1f); SetWindowPos(TSN->SNHwnd, nullptr, Ax, Ay, Aw, Ah, SWP_NOZORDER | SWP_NOACTIVATE); } } if (bVisible) { ShowWindow(TSN->SNHwnd, SW_SHOWNA); // when loading notes (only at startup), place all non-top notes at the bottom so they don't cover other windows if (pwszText && !bOnTop && bIsStartup) SetWindowPos(TSN->SNHwnd, HWND_BOTTOM, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE | SWP_ASYNCWINDOWPOS); } if (scrollV) SendMessage(TSN->REHwnd, EM_LINESCROLL, 0, scrollV); // make sure that any event triggered by init doesn't cause a meaningless save KillTimer(TSN->SNHwnd, 1025); if (!bLoading) NotifyList(); return TSN; } void NewNote(int Ax, int Ay, int Aw, int Ah, const wchar_t *pwszText, ULONGLONG *ID, BOOL bVisible, BOOL bOnTop, int scrollV) { auto *PSN = NewNoteEx(Ax, Ay, Aw, Ah, pwszText, ID, bVisible, bOnTop, scrollV, 0, 0, nullptr, nullptr, FALSE); if (PSN) SetFocus(PSN->REHwnd); } static void LoadNote(char *Value, bool bIsStartup) { // new eXtended/fleXible data format int scrollV = 0; STICKYNOTEFONT *pCustomFont = nullptr; char *DelPos = strchr(Value + 1, 0x1B); if (DelPos) *DelPos = 0; // id:x:y:w:h:flags char *TVal = strchr(Value + 1, ':'); if (!TVal || (DelPos && TVal > DelPos)) return; *TVal++ = 0; ULONGLONG id; id = _strtoui64(Value + 1, nullptr, 16); int rect[4]; for (auto &it : rect) { char *sep = strchr(TVal, ':'); if (!sep || (DelPos && sep > DelPos)) return; *sep++ = 0; it = strtol(TVal, nullptr, 10); TVal = sep; } BOOL bVisible = 0, bOnTop = 0; uint32_t flags = strtoul(TVal, nullptr, 16); if (flags & 1) bVisible = TRUE; if (flags & 2) bOnTop = TRUE; // optional \033 separated params CMStringW wszText; ptrW title; COLORREF BgColor = 0, FgColor = 0; while (DelPos) { TVal = DelPos + 1; // find param end and make sure it's null-terminated (if end of data then it's already null-terminated) DelPos = strchr(TVal, 0x1B); if (DelPos) *DelPos = 0; // tag: char *sep = strchr(TVal, ':'); if (!sep || (DelPos && sep > DelPos)) return; UINT tag = strtoul(TVal, nullptr, 10); TVal = sep + 1; switch (tag) { case DATATAG_TEXT: if (auto *pwszTmp = mir_utf8decodeW(TVal)) { wszText = pwszTmp; mir_free(pwszTmp); } else wszText = _A2T(TVal); break; case DATATAG_SCROLLPOS: scrollV = (int)strtoul(TVal, nullptr, 10); break; case DATATAG_BGCOL: BgColor = strtoul(TVal, nullptr, 16) | 0xff000000; break; case DATATAG_FGCOL: FgColor = strtoul(TVal, nullptr, 16) | 0xff000000; break; case DATATAG_TITLE: if (mir_strlen(TVal) > MAX_TITLE_LEN) TVal[MAX_TITLE_LEN] = 0; title = mir_a2u(TVal); break; case DATATAG_FONT: int fsize; UINT fstyle, fcharset; char *TVal2 = TVal; sep = strchr(TVal2, ':'); if (!sep || (DelPos && sep > DelPos)) return; *sep++ = 0; fsize = strtol(TVal2, nullptr, 10); TVal2 = sep; sep = strchr(TVal2, ':'); if (!sep || (DelPos && sep > DelPos)) return; *sep++ = 0; fstyle = strtoul(TVal2, nullptr, 10); TVal2 = sep; sep = strchr(TVal2, ':'); if (!sep || (DelPos && sep > DelPos)) return; *sep++ = 0; fcharset = strtoul(TVal2, nullptr, 10); TVal2 = sep; if (TVal2 >= DelPos) return; pCustomFont = (STICKYNOTEFONT*)malloc(sizeof(STICKYNOTEFONT)); pCustomFont->size = (char)fsize; pCustomFont->style = (uint8_t)fstyle; pCustomFont->charset = (uint8_t)fcharset; wcsncpy_s(pCustomFont->szFace, _A2T(TVal2), _TRUNCATE); pCustomFont->hFont = nullptr; if (!CreateStickyNoteFont(pCustomFont, nullptr)) { free(pCustomFont); pCustomFont = nullptr; } break; } } bVisible = bVisible && (!bIsStartup || g_plugin.bShowNotesAtStart); if (bIsStartup) bVisible |= 0x10000; NewNoteEx(rect[0], rect[1], rect[2], rect[3], wszText, &id, bVisible, bOnTop, scrollV, BgColor, FgColor, title, pCustomFont, TRUE); } void LoadNotes(bool bIsStartup) { g_arStickies.destroy(); int NotesCount = g_plugin.getDword("NotesData", 0); for (int i = 0; i < NotesCount; i++) { char ValueName[32]; mir_snprintf(ValueName, "NotesData%d", i); DBVARIANT dbv = {DBVT_BLOB}; if (db_get(0, MODULENAME, ValueName, &dbv)) continue; if (!dbv.cpbVal || !dbv.pbVal) continue; // skip old unused format if (dbv.pbVal[0] != 'X') continue; LoadNote((char*)dbv.pbVal, bIsStartup); db_free(&dbv); } NotifyList(); } ///////////////////////////////////////////////////////////////////////////////////////// static void EditNote(STICKYNOTE *SN) { if (!SN) return; if (!SN->bVisible) { SN->bVisible = TRUE; JustSaveNotes(); } SetWindowPos(SN->SNHwnd, HWND_TOPMOST, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE); if (!SN->bOnTop) SetWindowPos(SN->SNHwnd, HWND_NOTOPMOST, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE); SetFocus(SN->REHwnd); } wchar_t* GetPreviewString(const wchar_t *lpsz) { const int MaxLen = 80; static wchar_t s[MaxLen + 8]; if (!lpsz) return L""; // trim leading spaces while (iswspace(*lpsz)) lpsz++; size_t l = mir_wstrlen(lpsz); if (!l) return L""; if (l <= MaxLen) { mir_wstrcpy(s, lpsz); } else { mir_wstrncpy(s, lpsz, MaxLen); s[MaxLen] = '.'; s[MaxLen + 1] = '.'; s[MaxLen + 2] = '.'; s[MaxLen + 3] = 0; } // convert line breaks and tabs to spaces wchar_t *p = s; while (*p) { if (iswspace(*p)) *p = ' '; p++; } return s; } ///////////////////////////////////////////////////////////////////////////////////////// class CNotesListDlg : public CDlgBase { void InitListView() { int i = 0; wchar_t S1[128]; wchar_t *V = TranslateT("Visible"); wchar_t *T = TranslateT("Top"); m_list.SetHoverTime(700); m_list.SetExtendedListViewStyle(LVS_EX_FULLROWSELECT | LVS_EX_GRIDLINES); m_list.DeleteAllItems(); for (auto &pNote : g_arStickies) { LV_ITEM lvTIt; lvTIt.mask = LVIF_TEXT; if (!pNote->CustomTitle || !pNote->pwszTitle) GetTriggerTimeString(&pNote->ID, S1, _countof(S1), TRUE); lvTIt.iItem = i; lvTIt.iSubItem = 0; lvTIt.pszText = (pNote->CustomTitle && pNote->pwszTitle) ? pNote->pwszTitle : S1; m_list.InsertItem(&lvTIt); if (pNote->bVisible) { lvTIt.iItem = i; lvTIt.iSubItem = 1; lvTIt.pszText = V; m_list.SetItem(&lvTIt); } if (pNote->bOnTop) { lvTIt.iItem = i; lvTIt.iSubItem = 2; lvTIt.pszText = T; m_list.SetItem(&lvTIt); } lvTIt.iItem = i; lvTIt.iSubItem = 3; lvTIt.pszText = GetPreviewString(pNote->wszText); m_list.SetItem(&lvTIt); i++; } m_list.SetItemState(0, LVIS_SELECTED, LVIS_SELECTED); } CCtrlListView m_list; CCtrlButton btnNew; public: CNotesListDlg() : CDlgBase(g_plugin, IDD_LISTREMINDERS), m_list(this, IDC_LISTREMINDERS), btnNew(this, IDC_ADDNEWREMINDER) { SetMinSize(394, 300); btnNew.OnClick = Callback(this, &CNotesListDlg::onClick_New); } bool OnInitDialog() override { pListDialog = this; Window_SetIcon_IcoLib(m_hwnd, iconList[13].hIcolib); ShowWindow(GetDlgItem(m_hwnd, IDC_FILTER), SW_HIDE); SetWindowText(m_hwnd, TranslateT("Notes")); SetDlgItemText(m_hwnd, IDC_REMINDERDATA, L""); LV_COLUMN lvCol; lvCol.mask = LVCF_TEXT | LVCF_WIDTH; lvCol.pszText = TranslateT("Note text"); lvCol.cx = 150; m_list.InsertColumn( 0, &lvCol); lvCol.pszText = TranslateT("Top"); lvCol.cx = 20; m_list.InsertColumn(0, &lvCol); lvCol.pszText = TranslateT("Visible"); lvCol.cx = 20; m_list.InsertColumn(0, &lvCol); lvCol.pszText = TranslateT("Date/Title"); lvCol.cx = 165; m_list.InsertColumn(0, &lvCol); InitListView(); SetWindowLongPtr(GetDlgItem(m_list.GetHwnd(), 0), GWL_ID, IDC_LISTREMINDERS_HEADER); Utils_RestoreWindowPosition(m_hwnd, 0, MODULENAME, "ListNotes"); return true; } void OnDestroy() override { Utils_SaveWindowPosition(m_hwnd, 0, MODULENAME, "ListNotes"); Window_FreeIcon_IcoLib(m_hwnd); pListDialog = nullptr; } int Resizer(UTILRESIZECONTROL *urc) override { switch (urc->wId) { case IDC_LISTREMINDERS: return RD_ANCHORX_WIDTH | RD_ANCHORY_HEIGHT; case IDC_REMINDERDATA: return RD_ANCHORX_WIDTH | RD_ANCHORY_BOTTOM; } return RD_ANCHORX_RIGHT | RD_ANCHORY_BOTTOM; } void onClick_New(CCtrlButton *) { PluginMenuCommandAddNew(0, 0); } void onContextMenu() { int idx = m_list.GetSelectionMark(); STICKYNOTE *pNote = (idx != -1) ? &g_arStickies[idx] : nullptr; HMENU hMenuLoad = LoadMenuA(g_plugin.getInst(), "MNU_NOTELISTPOPUP"); HMENU FhMenu = GetSubMenu(hMenuLoad, 0); MENUITEMINFO mii = { 0 }; mii.cbSize = sizeof(mii); mii.fMask = MIIM_STATE; mii.fState = MFS_DEFAULT; if (!pNote) mii.fState |= MFS_GRAYED; SetMenuItemInfo(FhMenu, ID_CONTEXTMENUNOTE_EDITNOTE, FALSE, &mii); if (!pNote) { EnableMenuItem(FhMenu, ID_CONTEXTMENUNOTE_REMOVENOTE, MF_GRAYED | MF_BYCOMMAND); EnableMenuItem(FhMenu, ID_CONTEXTMENUNOTE_TOGGLEVISIBILITY, MF_GRAYED | MF_BYCOMMAND); EnableMenuItem(FhMenu, ID_CONTEXTMENUNOTE_TOGGLEONTOP, MF_GRAYED | MF_BYCOMMAND); } else { if (pNote->bVisible) CheckMenuItem(FhMenu, ID_CONTEXTMENUNOTE_TOGGLEVISIBILITY, MF_CHECKED | MF_BYCOMMAND); if (pNote->bOnTop) CheckMenuItem(FhMenu, ID_CONTEXTMENUNOTE_TOGGLEONTOP, MF_CHECKED | MF_BYCOMMAND); } TranslateMenu(FhMenu); RECT rc; m_list.GetItemRect(idx, &rc, LVIR_LABEL); POINT pt = { rc.left, rc.bottom }; ClientToScreen(m_list.GetHwnd(), &pt); int iSel = TrackPopupMenu(FhMenu, TPM_LEFTALIGN | TPM_RIGHTBUTTON, pt.x, pt.y, 0, m_hwnd, nullptr); DestroyMenu(hMenuLoad); switch (iSel) { case ID_CONTEXTMENUNOTE_EDITNOTE: if (m_list.GetSelectedCount()) { int i = m_list.GetSelectionMark(); if (i != -1) EditNote(&g_arStickies[i]); } break; case ID_CONTEXTMENUNOTE_TOGGLEVISIBILITY: if (m_list.GetSelectedCount()) { int i = m_list.GetSelectionMark(); if (i != -1) { auto &SN = g_arStickies[i]; SN.bVisible = !SN.bVisible; ShowWindow(SN.SNHwnd, SN.bVisible ? SW_SHOWNA : SW_HIDE); JustSaveNotes(); } } break; case ID_CONTEXTMENUNOTE_TOGGLEONTOP: if (m_list.GetSelectedCount()) { int i = m_list.GetSelectionMark(); if (i != -1) { auto &SN = g_arStickies[i]; SN.bOnTop = !SN.bOnTop; SetWindowPos(SN.SNHwnd, SN.bOnTop ? HWND_TOPMOST : HWND_NOTOPMOST, 0, 0, 0, 0, SWP_SHOWWINDOW | SWP_NOMOVE | SWP_NOSIZE); RedrawWindow(SN.SNHwnd, nullptr, nullptr, RDW_FRAME | RDW_INVALIDATE | RDW_UPDATENOW); JustSaveNotes(); } } break; case ID_CONTEXTMENUNOTE_NEWNOTE: PluginMenuCommandAddNew(0, 0); break; case ID_CONTEXTMENUNOTE_DELETEALLNOTES: PluginMenuCommandDeleteNotes(0, 0); break; case ID_CONTEXTMENUNOTE_REMOVENOTE: if (m_list.GetSelectedCount()) { int i = m_list.GetSelectionMark(); if (i != -1) OnDeleteNote(m_hwnd, &g_arStickies[i]); } break; case ID_CONTEXTMENUNOTE_SHOW: ShowHideNotes(); break; case ID_CONTEXTMENUNOTE_BRINGALLTOTOP: BringAllNotesToFront(nullptr); break; } } void Reload() { SetDlgItemTextA(m_hwnd, IDC_REMINDERDATA, ""); InitListView(); } void list_onItemChanged(CCtrlListView::TEventInfo *ev) { SetDlgItemTextW(m_hwnd, IDC_REMINDERDATA, g_arStickies[ev->nmlv->iItem].wszText); } void list_onDblClick(CCtrlListView::TEventInfo*) { if (m_list.GetSelectedCount()) { int i = m_list.GetSelectionMark(); if (i != -1) EditNote(&g_arStickies[i]); } } }; void CloseNotesList() { if (pListDialog) pListDialog->Close(); } static void NotifyList() { if (pListDialog) pListDialog->Reload(); } ///////////////////////////////////////////////////////////////////// // Notes List dialog (uses same dialog template as reminder list) INT_PTR PluginMenuCommandAddNew(WPARAM, LPARAM) { NewNote(0, 0, 0, 0, nullptr, nullptr, TRUE, TRUE, 0); return 0; } INT_PTR PluginMenuCommandShowHide(WPARAM, LPARAM) { ShowHideNotes(); return 0; } INT_PTR PluginMenuCommandViewNotes(WPARAM, LPARAM) { if (!pListDialog) (new CNotesListDlg())->Show(); else BringWindowToTop(pListDialog->GetHwnd()); return 0; } INT_PTR PluginMenuCommandAllBringFront(WPARAM, LPARAM) { BringAllNotesToFront(nullptr); return 0; } INT_PTR PluginMenuCommandDeleteNotes(WPARAM, LPARAM) { if (g_arStickies.getCount()) if (IDOK == MessageBox(nullptr, TranslateT("Are you sure you want to delete all notes?"), TranslateT(SECTIONNAME), MB_OKCANCEL)) DeleteNotes(); return 0; }