/* Copyright (c) 2005 Victor Pavlychko (nullbyte@sotline.net.ua) Copyright (C) 2012-23 Miranda NG team (https://miranda-ng.org) 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 version 2 of the License. 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, see . */ #include "stdafx.h" #define AVERAGE_ITEM_HEIGHT 100 HANDLE htuLog = 0; void InitHotkeys() { HOTKEYDESC hkd = {}; hkd.szSection.a = MODULENAME; hkd.szDescription.a = LPGEN("Toggle bookmark"); hkd.pszName = "ns_bookmark"; hkd.DefHotKey = HOTKEYCODE(HOTKEYF_CONTROL, 'B') | HKF_MIRANDA_LOCAL; hkd.lParam = HOTKEY_BOOKMARK; g_plugin.addHotkey(&hkd); hkd.szDescription.a = LPGEN("Search"); hkd.pszName = "ns_search"; hkd.DefHotKey = HOTKEYCODE(HOTKEYF_CONTROL, 'S') | HKF_MIRANDA_LOCAL; hkd.lParam = HOTKEY_SEARCH; g_plugin.addHotkey(&hkd); hkd.szDescription.a = LPGEN("Search forward"); hkd.pszName = "ns_seek_forward"; hkd.DefHotKey = HOTKEYCODE(0, VK_F3) | HKF_MIRANDA_LOCAL; hkd.lParam = HOTKEY_SEEK_FORWARD; g_plugin.addHotkey(&hkd); hkd.szDescription.a = LPGEN("Search backward"); hkd.pszName = "ns_seek_back"; hkd.DefHotKey = HOTKEYCODE(HOTKEYF_SHIFT, VK_F3) | HKF_MIRANDA_LOCAL; hkd.lParam = HOTKEY_SEEK_BACK; g_plugin.addHotkey(&hkd); } ///////////////////////////////////////////////////////////////////////////////////////// // Control utilities, types and constants NewstoryListData::NewstoryListData(HWND _1) : m_hwnd(_1), redrawTimer(Miranda_GetSystemWindow(), (LPARAM)this) { bSortAscending = g_plugin.bSortAscending; redrawTimer.OnEvent = Callback(this, &NewstoryListData::OnTimer); } void NewstoryListData::OnContextMenu(int index, POINT pt) { HMENU hMenu = NSMenu_Build(this, (index == -1) ? 0 : LoadItem(index)); if (pMsgDlg != nullptr && pMsgDlg->isChat()) Chat_CreateMenu(hMenu, pMsgDlg->getChat(), nullptr); TrackPopupMenu(hMenu, TPM_TOPALIGN | TPM_LEFTALIGN | TPM_LEFTBUTTON, pt.x, pt.y, 0, m_hwnd, nullptr); Menu_DestroyNestedMenu(hMenu); } void NewstoryListData::OnResize(int newWidth, int newHeight) { bool bDraw = false; if (newWidth != cachedWindowWidth) { cachedWindowWidth = newWidth; for (int i = 0; i < totalCount; i++) LoadItem(i)->savedHeight = -1; bDraw = true; } if (newHeight != cachedWindowHeight) { cachedWindowHeight = newHeight; FixScrollPosition(true); bDraw = true; } if (bDraw) InvalidateRect(m_hwnd, 0, FALSE); } void NewstoryListData::OnTimer(CTimer *pTimer) { pTimer->Stop(); if (bWasAtBottom) EnsureVisible(totalCount - 1); InvalidateRect(m_hwnd, 0, FALSE); } void NewstoryListData::AddChatEvent(SESSION_INFO *si, const LOGINFO *lin) { ScheduleDraw(); items.addChatEvent(si, lin); totalCount++; } void NewstoryListData::AddEvent(MCONTACT hContact, MEVENT hFirstEvent, int iCount) { ScheduleDraw(); items.addEvent(hContact, hFirstEvent, iCount); totalCount = items.getCount(); } void NewstoryListData::AddResults(const OBJLIST &results) { ScheduleDraw(); items.addResults(results); totalCount = items.getCount(); } void NewstoryListData::AddSelection(int iFirst, int iLast) { int start = min(totalCount - 1, iFirst); int end = min(totalCount - 1, max(0, iLast)); if (start > end) std::swap(start, end); for (int i = start; i <= end; ++i) if (auto *p = GetItem(i)) p->m_bSelected = true; InvalidateRect(m_hwnd, 0, FALSE); } bool NewstoryListData::AtBottom(void) const { if (scrollTopItem > cachedMaxTopItem) return true; if (scrollTopItem == cachedMaxTopItem && cachedMaxTopPixel >= scrollTopPixel) return true; return false; } bool NewstoryListData::AtTop(void) const { if (scrollTopItem < 0) return true; if (scrollTopItem == 0 && scrollTopPixel == 0) return true; return false; } ///////////////////////////////////////////////////////////////////////////////////////// // Edit box window procedure static LRESULT CALLBACK HistoryEditWndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam) { auto *pData = (NewstoryListData *)GetWindowLongPtr(GetParent(hwnd), 0); switch (msg) { case WM_KEYDOWN: switch (wParam) { case VK_RETURN: pData->EndEditItem(true); return 0; case VK_ESCAPE: pData->EndEditItem(false); return 0; } break; case WM_GETDLGCODE: if (lParam) { MSG *msg2 = (MSG *)lParam; if (msg2->message == WM_KEYDOWN && msg2->wParam == VK_TAB) return 0; if (msg2->message == WM_CHAR && msg2->wParam == '\t') return 0; } return DLGC_WANTMESSAGE; case WM_KILLFOCUS: pData->EndEditItem(false); return 0; } return mir_callNextSubclass(hwnd, HistoryEditWndProc, msg, wParam, lParam); } void NewstoryListData::BeginEditItem() { if (hwndEditBox) EndEditItem(false); if (scrollTopItem > caret) return; ItemData *item = LoadItem(caret); if (item->dbe.eventType != EVENTTYPE_MESSAGE) return; RECT rc; GetClientRect(m_hwnd, &rc); int height = rc.bottom - rc.top; int top = scrollTopPixel; int idx = scrollTopItem; int itemHeight = GetItemHeight(idx); while (top < height) { if (idx == caret) break; top += itemHeight; idx++; itemHeight = GetItemHeight(idx); } int fontid, colorid; item->getFontColor(fontid, colorid); // #4012 make sure that both single & double CRLF are now double CMStringW wszText(item->getWBuf()); wszText.Replace(L"\r\n", L"\n"); wszText.Replace(L"\n", L"\r\n"); uint32_t dwStyle = WS_CHILD | WS_BORDER | ES_MULTILINE | ES_AUTOVSCROLL; hwndEditBox = CreateWindow(L"EDIT", wszText, dwStyle, 0, top, rc.right - rc.left, itemHeight, m_hwnd, NULL, g_plugin.getInst(), NULL); mir_subclassWindow(hwndEditBox, HistoryEditWndProc); SendMessage(hwndEditBox, WM_SETFONT, (WPARAM)g_fontTable[fontid].hfnt, 0); SendMessage(hwndEditBox, EM_SETMARGINS, EC_RIGHTMARGIN, 100); ShowWindow(hwndEditBox, SW_SHOW); SetFocus(hwndEditBox); } ///////////////////////////////////////////////////////////////////////////////////////// void NewstoryListData::CalcBottom() { int maxTopItem = totalCount, tmp = 0; while (maxTopItem > 0 && tmp < cachedWindowHeight) tmp += GetItemHeight(--maxTopItem); cachedMaxTopItem = maxTopItem; cachedMaxTopPixel = (cachedWindowHeight < tmp) ? cachedWindowHeight - tmp : 0; } void NewstoryListData::Clear() { items.clear(); totalCount = 0; InvalidateRect(m_hwnd, 0, FALSE); } void NewstoryListData::ClearSelection(int iFirst, int iLast) { int start = min(0, iFirst); int end = (iLast <= 0) ? totalCount - 1 : iLast; if (start > end) std::swap(start, end); for (int i = start; i <= end; ++i) if (auto *pItem = GetItem(i)) pItem->m_bSelected = false; InvalidateRect(m_hwnd, 0, FALSE); } void NewstoryListData::Copy(bool bTextOnly) { Utils_ClipboardCopy(GatherSelected(bTextOnly)); } void NewstoryListData::CopyUrl() { if (auto *pItem = GetItem(caret)) { if (pItem->m_bOfflineDownloaded) { DB::EventInfo dbei(pItem->hEvent); DB::FILE_BLOB blob(dbei); Utils_ClipboardCopy(blob.getLocalName()); } else Srmm_DownloadOfflineFile(pItem->hContact, pItem->hEvent, OFD_COPYURL); } } void NewstoryListData::DeleteItems(void) { if (IDYES != MessageBoxW(m_hwnd, TranslateT("Are you sure to remove selected event(s)?"), _T(MODULETITLE), MB_YESNOCANCEL | MB_ICONQUESTION)) return; g_plugin.bDisableDelete = true; int firstSel = -1; for (int i = totalCount - 1; i >= 0; i--) { auto *p = GetItem(i); if (!p->m_bSelected) continue; if (p->hEvent) db_event_delete(p->hEvent); items.remove(i); totalCount--; firstSel = i; } g_plugin.bDisableDelete = false; if (firstSel != -1) { SetCaret(firstSel, false); SetSelection(firstSel, firstSel); FixScrollPosition(true); } } void NewstoryListData::Download(int options) { if (auto *p = LoadItem(caret)) Srmm_DownloadOfflineFile(p->hContact, p->hEvent, options); } void NewstoryListData::EndEditItem(bool bAccept) { if (hwndEditBox == nullptr) return; if (bAccept) { if ((GetWindowLong(hwndEditBox, GWL_STYLE) & ES_READONLY) == 0) { auto *pItem = GetItem(caret); int iTextLen = GetWindowTextLengthW(hwndEditBox); mir_free(pItem->wtext); pItem->wtext = (wchar_t *)mir_alloc((iTextLen + 1) * sizeof(wchar_t)); GetWindowTextW(hwndEditBox, pItem->wtext, iTextLen+1); pItem->wtext[iTextLen] = 0; if (pItem->hContact && pItem->hEvent) { DBEVENTINFO dbei = pItem->dbe; ptrA szUtf(mir_utf8encodeW(pItem->wtext)); dbei.cbBlob = (int)mir_strlen(szUtf) + 1; dbei.pBlob = (uint8_t *)szUtf.get(); db_event_edit(pItem->hEvent, &dbei); } MTextDestroy(pItem->data); pItem->data = 0; pItem->savedHeight = -1; pItem->checkCreate(m_hwnd); } } DestroyWindow(hwndEditBox); hwndEditBox = nullptr; } void NewstoryListData::EnsureVisible(int item) { if (scrollTopItem >= item) { scrollTopItem = item; scrollTopPixel = 0; } else { RECT rc; GetClientRect(m_hwnd, &rc); int height = rc.bottom - rc.top; int idx = scrollTopItem; int itemHeight = GetItemHeight(idx); int top = itemHeight + scrollTopPixel; bool found = false; while (top < height) { if (idx == item) { itemHeight = GetItemHeight(idx); found = true; break; } top += itemHeight; idx++; itemHeight = GetItemHeight(idx); } if (!found) { scrollTopItem = item; scrollTopPixel = 0; } } FixScrollPosition(); InvalidateRect(m_hwnd, 0, FALSE); } int NewstoryListData::FindNext(const wchar_t *pwszText) { int idx = items.FindNext(caret, Filter(Filter::EVENTONLY, pwszText)); if (idx == -1 && caret > 0) idx = items.FindNext(-1, Filter(Filter::EVENTONLY, pwszText)); if (idx >= 0) { SetSelection(idx, idx); SetCaret(idx); } return idx; } int NewstoryListData::FindPrev(const wchar_t *pwszText) { int idx = items.FindPrev(caret, Filter(Filter::EVENTONLY, pwszText)); if (idx == -1 && caret != totalCount - 1) idx = items.FindPrev(totalCount, Filter(Filter::EVENTONLY, pwszText)); if (idx >= 0) { SetSelection(idx, idx); SetCaret(idx); } return idx; } void NewstoryListData::FixScrollPosition(bool bForce) { EndEditItem(false); if (bForce || cachedMaxTopItem != scrollTopItem) CalcBottom(); if (scrollTopItem < 0) scrollTopItem = 0; if (bForce || scrollTopItem > cachedMaxTopItem || (scrollTopItem == cachedMaxTopItem && scrollTopPixel < cachedMaxTopPixel)) { scrollTopItem = cachedMaxTopItem; scrollTopPixel = cachedMaxTopPixel; } } CMStringW NewstoryListData::GatherSelected(bool bTextOnly) { CMStringW ret; int eventCount = totalCount; for (int i = 0; i < eventCount; i++) { ItemData *p = GetItem(i); if (!p->m_bSelected) continue; CMStringW wszText(bTextOnly ? p->wtext : p->formatString()); RemoveBbcodes(wszText); ret.Append(wszText); ret.Append(L"\r\n"); } return ret; } ItemData* NewstoryListData::GetItem(int idx) const { if (totalCount == 0) return nullptr; return (bSortAscending) ? items.get(idx, false) : items.get(totalCount - 1 - idx, false); } int NewstoryListData::GetItemFromPixel(int yPos) { if (!totalCount) return -1; RECT rc; GetClientRect(m_hwnd, &rc); int height = rc.bottom - rc.top; int current = scrollTopItem; int top = scrollTopPixel; int bottom = top + GetItemHeight(current); while (top <= height) { if (yPos >= top && yPos <= bottom) return current; if (++current >= totalCount) break; top = bottom; bottom = top + GetItemHeight(current); } return -1; } int NewstoryListData::GetItemHeight(int index) { auto *item = LoadItem(index); if (!item) return 0; if (item->savedHeight >= 0) return item->savedHeight; int fontid, colorid; item->getFontColor(fontid, colorid); item->checkCreate(m_hwnd); HDC hdc = GetDC(m_hwnd); HFONT hOldFont = (HFONT)SelectObject(hdc, g_fontTable[fontid].hfnt); RECT rc; GetClientRect(m_hwnd, &rc); int width = rc.right - rc.left; SIZE sz = { width - 6, 0 }; MTextMeasure(hdc, &sz, item->data); SelectObject(hdc, hOldFont); ReleaseDC(m_hwnd, hdc); return item->savedHeight = sz.cy + 5; } bool NewstoryListData::HasSelection() const { for (int i = 0; i < totalCount; i++) if (auto *p = GetItem(i)) if (p->m_bSelected) return true; return false; } void NewstoryListData::HitTotal(int y) { int i = 0; while (i < totalCount && y > 0) y -= GetItemHeight(i++); scrollTopItem = i; scrollTopPixel = y; FixScrollPosition(); } ItemData* NewstoryListData::LoadItem(int idx) { if (totalCount == 0) return nullptr; return (bSortAscending) ? items.get(idx, true) : items.get(totalCount - 1 - idx, true); } void NewstoryListData::OpenFolder() { if (auto *pItem = GetItem(caret)) { if (pItem->m_bOfflineDownloaded) { DB::EventInfo dbei(pItem->hEvent); DB::FILE_BLOB blob(dbei); CMStringW wszFile(blob.getLocalName()); int idx = wszFile.ReverseFind('\\'); if (idx != -1) wszFile.Truncate(idx); ::ShellExecute(nullptr, L"open", wszFile, nullptr, nullptr, SW_SHOWNORMAL); } } } int NewstoryListData::PaintItem(HDC hdc, int index, int top, int width) { auto *item = LoadItem(index); item->savedTop = top; // remove any selections that might be created by the BBCodes parser MTextSendMessage(0, item->data, EM_SETSEL, 0, 0); // LOGFONT lfText; COLORREF clText, clBack, clLine; int fontid, colorid; item->getFontColor(fontid, colorid); clText = g_fontTable[fontid].cl; if (item->m_bHighlighted) { clText = g_fontTable[FONT_HIGHLIGHT].cl; clBack = g_colorTable[COLOR_HIGHLIGHT_BACK].cl; clLine = g_colorTable[COLOR_FRAME].cl; } else if (item->m_bSelected) { clText = g_colorTable[COLOR_SELTEXT].cl; clBack = g_colorTable[COLOR_SELBACK].cl; clLine = g_colorTable[COLOR_SELFRAME].cl; } else { clLine = g_colorTable[COLOR_FRAME].cl; clBack = g_colorTable[colorid].cl; } item->checkCreate(m_hwnd); SIZE sz; sz.cx = width - 6; HFONT hfnt = (HFONT)SelectObject(hdc, g_fontTable[fontid].hfnt); MTextMeasure(hdc, &sz, item->data); SelectObject(hdc, hfnt); int height = sz.cy + 5; RECT rc; SetRect(&rc, 0, top, width, top + height); HBRUSH hbr = CreateSolidBrush(clBack); FillRect(hdc, &rc, hbr); DeleteObject(hbr); SetTextColor(hdc, clText); SetBkMode(hdc, TRANSPARENT); POINT pos; pos.x = 3; pos.y = top + 2; hfnt = (HFONT)SelectObject(hdc, g_fontTable[fontid].hfnt); MTextDisplay(hdc, pos, sz, item->data); SelectObject(hdc, hfnt); HPEN hpn = (HPEN)SelectObject(hdc, CreatePen(PS_SOLID, 1, clLine)); MoveToEx(hdc, rc.left, rc.bottom - 1, 0); LineTo(hdc, rc.right, rc.bottom - 1); DeleteObject(SelectObject(hdc, hpn)); return height; } void NewstoryListData::RecalcScrollBar() { if (totalCount == 0) return; int yTotal = 0, yTop = 0; for (int i = 0; i < totalCount; i++) { if (i == scrollTopItem) yTop = yTotal - scrollTopPixel; yTotal += GetItemHeight(i); } SCROLLINFO si = {}; si.cbSize = sizeof(si); si.fMask = SIF_ALL; si.nMin = 0; si.nMax = yTotal; si.nPage = cachedWindowHeight; si.nPos = yTop; if (si.nPos != cachedScrollbarPos || si.nMax != cachedScrollbarMax) { cachedScrollbarPos = si.nPos; cachedScrollbarMax = si.nMax; SetScrollInfo(m_hwnd, SB_VERT, &si, TRUE); } } void NewstoryListData::Quote() { if (pMsgDlg) { CMStringW wszText(GatherSelected(true)); RemoveBbcodes(wszText); pMsgDlg->SetMessageText(Srmm_Quote(wszText)); SetFocus(pMsgDlg->GetInput()); } } void NewstoryListData::Reply() { if (pMsgDlg) if (auto *pItem = GetItem(caret)) pMsgDlg->SetQuoteEvent(pItem->hEvent); } void NewstoryListData::ScheduleDraw() { bWasAtBottom = AtBottom(); redrawTimer.Stop(); redrawTimer.Start(30); } void NewstoryListData::SetCaret(int idx, bool bEnsureVisible) { if (idx < totalCount) { caret = idx; if (bEnsureVisible) EnsureVisible(idx); } } void NewstoryListData::SetContact(MCONTACT hContact) { WindowList_Add(g_hNewstoryLogs, m_hwnd, hContact); } void NewstoryListData::SetDialog(CSrmmBaseDialog *pDlg) { if (pMsgDlg = pDlg) SetContact(pDlg->m_hContact); } void NewstoryListData::SetPos(int pos) { SetSelection((selStart == -1) ? pos : selStart, pos); SetCaret(pos); } void NewstoryListData::SetSelection(int iFirst, int iLast) { int start = min(totalCount - 1, iFirst); int end = min(totalCount - 1, max(0, iLast)); if (start > end) std::swap(start, end); int count = totalCount; for (int i = 0; i < count; ++i) { auto *p = GetItem(i); if (i >= start && i <= end) p->m_bSelected = true; else p->m_bSelected = false; } InvalidateRect(m_hwnd, 0, FALSE); } void NewstoryListData::ToggleBookmark() { int eventCount = totalCount; for (int i = 0; i < eventCount; i++) { ItemData *p = GetItem(i); if (!p->m_bSelected) continue; if (p->dbe.flags & DBEF_BOOKMARK) p->dbe.flags &= ~DBEF_BOOKMARK; else p->dbe.flags |= DBEF_BOOKMARK; db_event_edit(p->hEvent, &p->dbe); p->setText(); } InvalidateRect(m_hwnd, 0, FALSE); } void NewstoryListData::ToggleSelection(int iFirst, int iLast) { int start = min(totalCount - 1, iFirst); int end = min(totalCount - 1, max(0, iLast)); if (start > end) std::swap(start, end); for (int i = start; i <= end; ++i) { auto *p = GetItem(i); p->m_bSelected = !p->m_bSelected; } InvalidateRect(m_hwnd, 0, FALSE); } void NewstoryListData::TryUp(int iCount) { if (totalCount == 0) return; auto *pTop = GetItem(0); MCONTACT hContact = pTop->hContact; if (pTop->hEvent == 0 || hContact == 0) return; int i; for (i = 0; i < iCount; i++) { MEVENT hPrev = db_event_prev(hContact, pTop->hEvent); if (hPrev == 0) break; auto *p = items.insert(0); p->hContact = hContact; p->hEvent = hPrev; totalCount++; } ItemData *pPrev = nullptr; for (int j = 0; j < i + 1; j++) { auto *pItem = GetItem(j); pPrev = pItem->checkNext(pPrev); } caret = 0; CalcBottom(); FixScrollPosition(); InvalidateRect(m_hwnd, 0, FALSE); } ///////////////////////////////////////////////////////////////////////////////////////// // Navigation by coordinates void NewstoryListData::LineUp() { if (AtTop()) TryUp(1); else ScrollUp(10); } void NewstoryListData::LineDown() { if (!AtBottom()) ScrollDown(10); } void NewstoryListData::PageUp() { if (AtTop()) TryUp(10); else ScrollUp(cachedWindowHeight); } void NewstoryListData::PageDown() { if (!AtBottom()) ScrollDown(cachedWindowHeight); } ///////////////////////////////////////////////////////////////////////////////////////// // Navigation by events void NewstoryListData::EventUp() { if (caret == 0) TryUp(1); else SetPos(caret - 1); } void NewstoryListData::EventDown() { if (caret < totalCount-1) SetPos(caret + 1); } void NewstoryListData::EventPageUp() { if (caret >= 10) SetPos(caret - 10); else TryUp(caret == 10 ? 1 : 10 - caret); } void NewstoryListData::EventPageDown() { if (caret < totalCount - 10) SetPos(caret + 10); else SetPos(totalCount - 1); } ///////////////////////////////////////////////////////////////////////////////////////// // Common navigation functions void NewstoryListData::ScrollBottom() { if (!totalCount) return; scrollTopItem = cachedMaxTopItem; scrollTopPixel = cachedMaxTopPixel; FixScrollPosition(true); InvalidateRect(m_hwnd, 0, FALSE); } void NewstoryListData::ScrollDown(int deltaY) { int iHeight = GetItemHeight(scrollTopItem) + scrollTopPixel; if (iHeight > deltaY) scrollTopPixel -= deltaY; else { deltaY -= iHeight; bool bFound = false; for (int i = scrollTopItem + 1; i < totalCount; i++) { iHeight = GetItemHeight(i); if (iHeight > deltaY) { scrollTopPixel = -deltaY; scrollTopItem = i; bFound = true; break; } deltaY -= iHeight; } if (!bFound) scrollTopItem = scrollTopPixel = 0; } FixScrollPosition(); InvalidateRect(m_hwnd, 0, FALSE); } void NewstoryListData::ScrollTop() { scrollTopItem = scrollTopPixel = 0; FixScrollPosition(); InvalidateRect(m_hwnd, 0, FALSE); } void NewstoryListData::ScrollUp(int deltaY) { int reserveY = -scrollTopPixel; // distance in pixels between the top event beginning and the window top coordinate if (reserveY >= deltaY) scrollTopPixel += deltaY; // stay on the same event, just move up else { deltaY -= reserveY; // move to the appropriate event first, then calculate the gap bool bFound = false; for (int i = scrollTopItem - 1; i >= 0; i--) { int iHeight = GetItemHeight(i); if (iHeight > deltaY) { scrollTopPixel = deltaY - iHeight; scrollTopItem = i; bFound = true; break; } deltaY -= iHeight; } if (!bFound) scrollTopItem = scrollTopPixel = 0; } FixScrollPosition(); InvalidateRect(m_hwnd, 0, FALSE); } ///////////////////////////////////////////////////////////////////////////////////////// // NewStory history control window procedure LRESULT CALLBACK NewstoryListWndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam) { int idx; POINT pt; NewstoryListData *data = (NewstoryListData *)GetWindowLongPtr(hwnd, 0); MSG message = { hwnd, msg, wParam, lParam }; switch (Hotkey_Check(&message, MODULENAME)) { case HOTKEY_SEEK_FORWARD: PostMessage(GetParent(hwnd), WM_COMMAND, MAKELONG(IDOK, BN_CLICKED), 1); break; case HOTKEY_SEEK_BACK: PostMessage(GetParent(hwnd), WM_COMMAND, MAKELONG(IDC_FINDPREV, BN_CLICKED), 1); break; case HOTKEY_SEARCH: PostMessage(GetParent(hwnd), WM_COMMAND, MAKELONG(IDC_SEARCH, BN_CLICKED), 1); break; case HOTKEY_BOOKMARK: data->ToggleBookmark(); return 0; } switch (msg) { case WM_CREATE: data = new NewstoryListData(hwnd); SetWindowLongPtr(hwnd, 0, (LONG_PTR)data); if (!g_plugin.bOptVScroll) SetWindowLong(hwnd, GWL_STYLE, GetWindowLong(hwnd, GWL_STYLE) & ~WS_VSCROLL); break; // History list control messages case NSM_SELECTITEMS: data->AddSelection(wParam, lParam); return 0; case NSM_SEEKTIME: { int eventCount = data->totalCount; for (int i = 0; i < eventCount; i++) { auto *p = data->GetItem(i); if (p->dbe.timestamp >= wParam) { data->SetSelection(i, i); data->SetCaret(i); break; } if (i == eventCount - 1) { data->SetSelection(i, i); data->SetCaret(i); } } } return TRUE; case NSM_ADDEVENT: data->AddEvent(wParam, lParam, 1); break; case NSM_SET_OPTIONS: data->bSortAscending = g_plugin.bSortAscending; data->scrollTopPixel = 0; data->FixScrollPosition(true); InvalidateRect(hwnd, 0, FALSE); break; case UM_ADDEVENT: if (data->pMsgDlg == nullptr) data->AddEvent(wParam, lParam, 1); break; case UM_EDITEVENT: idx = data->items.find(lParam); if (idx != -1) { auto *p = data->GetItem(idx); p->load(true); p->setText(); InvalidateRect(hwnd, 0, FALSE); } break; case UM_REMOVEEVENT: idx = data->items.find(lParam); if (idx != -1) { data->items.remove(idx); data->totalCount--; data->FixScrollPosition(true); InvalidateRect(hwnd, 0, FALSE); } break; case WM_SIZE: data->OnResize(LOWORD(lParam), HIWORD(lParam)); break; case WM_COMMAND: if (NSMenu_Process(LOWORD(wParam), data)) return 1; break; case WM_ERASEBKGND: return 1; case WM_PAINT: /* we get so many InvalidateRect()'s that there is no point painting, Windows in theory shouldn't queue up WM_PAINTs in this case but it does so we'll just ignore them */ if (IsWindowVisible(hwnd)) { PAINTSTRUCT ps; HDC hdcWindow = BeginPaint(hwnd, &ps); RECT rc; GetClientRect(hwnd, &rc); HDC hdc = CreateCompatibleDC(hdcWindow); HBITMAP hbmSave = (HBITMAP)SelectObject(hdc, CreateCompatibleBitmap(hdcWindow, rc.right - rc.left, rc.bottom - rc.top)); int height = rc.bottom - rc.top; int width = rc.right - rc.left; int top = data->scrollTopPixel; idx = data->scrollTopItem; while ((top < height) && (idx < data->totalCount)) top += data->PaintItem(hdc, idx++, top, width); data->cachedMaxDrawnItem = idx; if (top <= height) { RECT rc2; SetRect(&rc2, 0, top, width, height); HBRUSH hbr = CreateSolidBrush(g_colorTable[COLOR_BACK].cl); FillRect(hdc, &rc2, hbr); DeleteObject(hbr); } if (g_plugin.bOptVScroll) data->RecalcScrollBar(); if (g_plugin.bDrawEdge) DrawEdge(hdc, &rc, BDR_SUNKENOUTER, BF_RECT); BitBlt(hdcWindow, 0, 0, rc.right, rc.bottom, hdc, 0, 0, SRCCOPY); DeleteObject(SelectObject(hdc, hbmSave)); DeleteDC(hdc); EndPaint(hwnd, &ps); } break; case WM_CONTEXTMENU: pt = { GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam) }; if (pt.x == -1 && pt.y == -1) GetCursorPos(&pt); POINT pt2 = pt; ScreenToClient(hwnd, &pt2); idx = data->GetItemFromPixel(pt2.y); if (idx != -1) { if (data->caret != idx) data->EndEditItem(false); data->SetCaret(idx); if (!data->HasSelection()) data->SetSelection(idx, idx); } data->OnContextMenu(idx, pt); break; case WM_KILLFOCUS: if (wParam && (HWND)wParam != data->hwndEditBox) data->EndEditItem(false); if (data->pMsgDlg && ((HWND)wParam == data->pMsgDlg->GetInput() || (HWND)wParam == data->pMsgDlg->GetHwnd())) data->ClearSelection(0, -1); return 0; case WM_SETFOCUS: return 0; case WM_GETDLGCODE: if (lParam) { MSG *msg2 = (MSG *)lParam; if (msg2->message == WM_KEYDOWN) { if (msg2->wParam == VK_TAB) return 0; if (msg2->wParam == VK_ESCAPE && !data->hwndEditBox) return 0; } else if (msg2->message == WM_CHAR) { if (msg2->wParam == '\t') return 0; if (msg2->wParam == 27 && !data->hwndEditBox) return 0; } } return DLGC_WANTMESSAGE; case WM_KEYDOWN: { bool isShift = (GetKeyState(VK_SHIFT) & 0x80) != 0; bool isCtrl = (GetKeyState(VK_CONTROL) & 0x80) != 0; if (!data->bWasShift && isShift) data->selStart = data->caret; else if (data->bWasShift && !isShift) data->selStart = -1; data->bWasShift = isShift; switch (wParam) { case VK_UP: if (g_plugin.bHppCompat) data->EventUp(); else data->LineUp(); break; case VK_DOWN: if (g_plugin.bHppCompat) data->EventDown(); else data->LineDown(); break; case VK_PRIOR: if (isCtrl) data->ScrollTop(); else if (g_plugin.bHppCompat) data->EventPageUp(); else data->PageUp(); break; case VK_NEXT: if (isCtrl) data->ScrollBottom(); else if (g_plugin.bHppCompat) data->EventPageDown(); else data->PageDown(); break; case VK_HOME: data->ScrollTop(); break; case VK_END: data->ScrollBottom(); break; case VK_F2: data->BeginEditItem(); break; case VK_ESCAPE: if (data->hwndEditBox) data->EndEditItem(false); break; case VK_DELETE: data->DeleteItems(); break; case VK_INSERT: case 'C': if (isCtrl) data->Copy(); break; case 'A': if (isCtrl) data->AddSelection(0, data->totalCount); break; } } break; case WM_LBUTTONDOWN: pt = { GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam) }; idx = data->GetItemFromPixel(pt.y); if (idx >= 0) { if (data->caret != idx) data->EndEditItem(false); auto *pItem = data->LoadItem(idx); if (wParam & MK_CONTROL) { data->ToggleSelection(idx, idx); data->SetCaret(idx); } else if (wParam & MK_SHIFT) { data->AddSelection(data->caret, idx); data->SetCaret(idx); } else { pt.y -= pItem->savedTop; CMStringW wszUrl; if (pItem->isLink(pt, &wszUrl)) { Utils_OpenUrlW(wszUrl); return 0; } data->selStart = idx; data->SetSelection(idx, idx); data->SetCaret(idx); } } SetFocus(hwnd); return 0; case WM_LBUTTONUP: pt = { GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam) }; idx = data->GetItemFromPixel(pt.y); if (idx >= 0) data->selStart = -1; break; case WM_LBUTTONDBLCLK: pt = { GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam) }; idx = data->GetItemFromPixel(pt.y); if (idx >= 0) { if (data->caret != idx) data->EndEditItem(false); auto *pItem = data->LoadItem(idx); pt.y -= pItem->savedTop; if (pItem->m_bOfflineFile) { Srmm_DownloadOfflineFile(pItem->hContact, pItem->hEvent, OFD_DOWNLOAD | OFD_RUN); return 0; } if (data->caret == idx) { data->BeginEditItem(); return 0; } } SetFocus(hwnd); return 0; case WM_MOUSEMOVE: pt = { GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam) }; idx = data->GetItemFromPixel(pt.y); if (idx >= 0) { auto *pItem = data->LoadItem(idx); MTextSendMessage(hwnd, pItem->data, msg, wParam, lParam); HCURSOR hOldCursor = GetCursor(); HCURSOR hNewCursor = LoadCursor(0, (pItem->isLink(pt) || pItem->m_bOfflineFile) ? IDC_HAND : IDC_ARROW); if (hOldCursor != hNewCursor) SetCursor(hNewCursor); if (data->selStart != -1) { data->SetSelection(data->selStart, idx); InvalidateRect(hwnd, 0, FALSE); } } break; case WM_MOUSEWHEEL: if ((short)HIWORD(wParam) < 0) data->LineDown(); else data->LineUp(); return TRUE; case WM_VSCROLL: { int s_scrollTopItem = data->scrollTopItem; int s_scrollTopPixel = data->scrollTopPixel; switch (LOWORD(wParam)) { case SB_LINEUP: if (g_plugin.bHppCompat) data->EventUp(); else data->LineUp(); break; case SB_LINEDOWN: if (g_plugin.bHppCompat) data->EventDown(); else data->LineDown(); break; case SB_PAGEUP: if (g_plugin.bHppCompat) data->EventPageUp(); else data->PageUp(); break; case SB_PAGEDOWN: if (g_plugin.bHppCompat) data->EventPageDown(); else data->PageDown(); break; case SB_BOTTOM: data->ScrollBottom(); break; case SB_TOP: data->ScrollTop(); break; case SB_THUMBTRACK: SCROLLINFO si; si.cbSize = sizeof(si); si.fMask = SIF_ALL; GetScrollInfo(hwnd, SB_VERT, &si); data->HitTotal(si.nTrackPos); break; default: return 0; } if (s_scrollTopItem != data->scrollTopItem || s_scrollTopPixel != data->scrollTopPixel) InvalidateRect(hwnd, 0, FALSE); } break; case WM_CTLCOLORSTATIC: case WM_CTLCOLOREDIT: if (lParam == INT_PTR(data->hwndEditBox)) { SetBkColor((HDC)wParam, g_colorTable[COLOR_SELBACK].cl); return (LRESULT)g_plugin.hBackBrush; } break; case WM_DESTROY: WindowList_Add(g_hNewstoryLogs, hwnd); delete data; SetWindowLongPtr(hwnd, 0, 0); break; } return DefWindowProc(hwnd, msg, wParam, lParam); } void InitNewstoryControl() { htuLog = MTextRegister("Newstory", MTEXT_FANCY_DEFAULT | MTEXT_SYSTEM_HICONS | MTEXT_FANCY_SMILEYS); WNDCLASS wndclass = {}; wndclass.style = /*CS_HREDRAW | CS_VREDRAW | */CS_DBLCLKS | CS_GLOBALCLASS; wndclass.lpfnWndProc = NewstoryListWndProc; wndclass.cbWndExtra = sizeof(void *); wndclass.hInstance = g_plugin.getInst(); wndclass.hCursor = LoadCursor(NULL, IDC_ARROW); wndclass.lpszClassName = _T(NEWSTORYLIST_CLASS); RegisterClass(&wndclass); }