/* Copyright (C) 2012-24 Miranda NG team (https://miranda-ng.org) This program is free software; you can redistribute it &/| 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 | 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" ///////////////////////////////////////////////////////////////////////////////////////// // patterns bool QSMainDlg::CheckPattern(CRowItem *pRow) { if (m_patterns.getCount() == 0) return true; for (auto &p : m_patterns) p->res = 0; int i = 0; for (auto &it : g_plugin.m_columns) { if (it->bEnabled && it->bFilter && pRow->pValues[i].text != nullptr) { CMStringW buf(pRow->pValues[i].text); buf.MakeLower(); for (auto &p : m_patterns) if (!p->res && buf.Find(p->str) != -1) p->res = true; } i++; } bool result = true; for (auto &p : m_patterns) result = result && p->res; return result; } void QSMainDlg::MakePattern(const wchar_t *pwszPattern) { m_patterns.destroy(); if (mir_wstrlen(pwszPattern) == 0) return; // m_wszPatternBuf works as a storage for patterns, we store pointers to it in m_patterns m_wszPatternBuf = mir_wstrdup(pwszPattern); CharLowerW(m_wszPatternBuf); for (wchar_t *p = m_wszPatternBuf; *p; ) { auto *pWord = wcspbrk(p, L" \""); if (pWord == nullptr) { m_patterns.insert(new Pattern(p)); return; } bool isSpace = pWord[0] == ' '; // there's some valuable info between p and pWord if (pWord != p) { *pWord = 0; m_patterns.insert(new Pattern(p)); } if (isSpace) { p = ltrimpw(pWord + 1); // skip all spaces } else { auto *pEnd = wcschr(++pWord, '\"'); // treat the rest of line as one pattern if (pEnd == nullptr) { m_patterns.insert(new Pattern(pWord)); return; } *pEnd = 0; m_patterns.insert(new Pattern(pWord)); p = ltrimpw(pEnd + 1); } } } ///////////////////////////////////////////////////////////////////////////////////////// void QSMainDlg::AddColumn(int idx, ColumnItem *pCol) { LV_COLUMN lvc = {}; lvc.mask = LVCF_TEXT | LVCF_WIDTH; lvc.pszText = TranslateW(pCol->title); lvc.cx = pCol->width; m_grid.InsertColumn(idx, &lvc); HDITEM hdi; hdi.mask = HDI_FORMAT; hdi.fmt = HDF_LEFT | HDF_STRING | HDF_CHECKBOX | (pCol->bFilter ? HDF_CHECKED : 0); SendMessage(m_grid.GetHeader(), HDM_SETITEM, idx, LPARAM(&hdi)); } void QSMainDlg::AddContactToList(MCONTACT hContact, CRowItem *pRow) { LV_ITEM li = {}; li.mask = LVIF_PARAM; li.iItem = 100000; li.lParam = LPARAM(pRow); li.iItem = m_grid.InsertItem(&li); li.iImage = 0; li.iSubItem = 0; for (int i = 0; i < g_plugin.m_columns.getCount(); i++) { auto &col = g_plugin.m_columns[i]; if (!col.bEnabled) continue; // Client icons preprocess li.pszText = pRow->pValues[i].text; li.mask = LVIF_TEXT + col.HasImage(li.pszText); if (col.isAccount) li.iImage = Clist_GetContactIcon(hContact); m_grid.SetItem(&li); li.iSubItem++; } } void QSMainDlg::AdvancedFilter() { m_grid.SetDraw(false); for (auto &it : m_rows) { bool bShow = (szFilterProto == nullptr) || !mir_strcmp(szFilterProto, it->szProto); if (bShow && !bShowOffline && it->status == ID_STATUS_OFFLINE) bShow = false; if (it->bPattern) { if (bShow) { if (!it->bActive) ProcessLine(it, false); } else { it->bActive = false; m_grid.DeleteItem(FindItem(it)); } } } m_grid.SetDraw(true); InvalidateRect(m_grid.GetHwnd(), 0, false); Sort(); UpdateSB(); } void QSMainDlg::ChangeStatusPicture(CRowItem *pRow, MCONTACT, LPARAM lParam) { int idx = FindItem(pRow); if (idx == -1) return; LV_ITEM li = {}; li.iItem = idx; li.mask = LVIF_IMAGE; li.iImage = lParam; m_grid.SetItem(&li); } void QSMainDlg::CopyMultiLines() { CMStringW buf; int i = 0; for (auto &it : g_plugin.m_columns) { if (it->bEnabled) { it->width = m_grid.GetColumnWidth(i); if (it->width >= 10) buf.AppendFormat(L"%s\t", it->title); } i++; } buf.Append(L"\r\n"); int nRows = m_grid.GetItemCount(); int nSelected = m_grid.GetSelectedCount(); for (int j = 0; j < nRows; j++) { if (nSelected > 1 && !m_grid.GetItemState(j, LVIS_SELECTED)) continue; auto *pRow = GetRow(j); i = 0; for (auto &it : g_plugin.m_columns) { if (it->bEnabled && it->width >= 10) buf.AppendFormat(L"%s\t", pRow->pValues[i].getText()); i++; } buf.Append(L"\r\n"); } Utils_ClipboardCopy(MClipUnicode(buf)); } void QSMainDlg::DeleteByList() { if (IDOK != MessageBoxW(0, TranslateT("Do you really want to delete selected contacts?"), TranslateT("Warning"), MB_OKCANCEL + MB_ICONWARNING)) return; m_grid.SetDraw(false); for (int i = m_grid.GetItemCount() - 1; i >= 0; i--) if (m_grid.GetItemState(i, LVIS_SELECTED)) db_delete_contact(GetRow(i)->hContact); m_grid.SetDraw(true); } void QSMainDlg::DeleteOneContact(MCONTACT hContact) { if (ServiceExists(MS_CLIST_DELETECONTACT)) CallService(MS_CLIST_DELETECONTACT, hContact, 0); else db_delete_contact(hContact); } wchar_t* QSMainDlg::DoMeta(MCONTACT hContact) { for (auto &it : m_rows) { if (it->hContact != hContact) continue; if (it->bIsMeta) { if (it->wparam == 0) it->wparam = ++hLastMeta; } else if (it->bIsSub) it->lparam = FindMeta(db_mc_getMeta(hContact), it->wparam); if (it->wparam > 0) { CMStringW tmp(FORMAT, L"[%d]", int(it->wparam)); if (it->lparam > 0) tmp.AppendFormat(L" %d", int(it->lparam)); return tmp.Detach(); } break; } return nullptr; } void QSMainDlg::DrawSB() { CStatusBarItem global(0, 0); for (auto &it : m_sbdata) { global.found += it->found; global.liston += it->liston; global.online += it->online; global.total += it->total; } CMStringW buf(FORMAT, TranslateT("%i users found (%i) Online: %i"), global.found, m_rows.getCount(), global.online); RECT rc; HDC hdc = GetDC(hwndStatusBar); DrawTextW(hdc, buf, buf.GetLength(), &rc, DT_CALCRECT); ReleaseDC(hwndStatusBar, hdc); int all = rc.right - rc.left, i = 1; mir_ptr parts((int*)mir_alloc(sizeof(int) * (m_sbdata.getCount()+2))); parts[0] = all; for (auto &it : m_sbdata) { UNREFERENCED_PARAMETER(it); all += 55; parts[i++] = all; } parts[i] = -1; SendMessageW(hwndStatusBar, SB_SETPARTS, m_sbdata.getCount() + 2, LPARAM(parts.get())); SendMessageW(hwndStatusBar, SB_SETTEXTW, 0, LPARAM(buf.c_str())); i = 1; for (auto &it : m_sbdata) { HICON hIcon; wchar_t c, *pc; if (it->bAccDel) { c = '!'; pc = TranslateT("deleted"); hIcon = Skin_LoadProtoIcon(it->szProto, ID_STATUS_OFFLINE); } else if (it->bAccOff) { c = '?'; pc = TranslateT("disabled"); hIcon = Skin_LoadProtoIcon(it->szProto, ID_STATUS_OFFLINE); } else { c = ' '; pc = TranslateT("active"); hIcon = Skin_LoadProtoIcon(it->szProto, ID_STATUS_ONLINE); } SendMessageW(hwndStatusBar, SB_SETICON, i, (LPARAM)hIcon); buf.Format(L"%c %d", c, it->found); SendMessageW(hwndStatusBar, SB_SETTEXTW, i, LPARAM(buf.c_str())); auto *pa = Proto_GetAccount(it->szProto); buf.Format(L"%s (%s): %d (%d); %s %d (%d))", pa->tszAccountName, pc, it->found, it->total, TranslateT("Online"), it->liston, it->online); SendMessageW(hwndStatusBar, SB_SETTIPTEXTW, i, LPARAM(buf.c_str())); i++; } } void QSMainDlg::FillGrid() { m_grid.SetDraw(false); for (auto &it: m_rows) ProcessLine(it); m_grid.SetDraw(true); InvalidateRect(m_grid.GetHwnd(), 0, FALSE); Sort(); UpdateSB(); AdvancedFilter(); m_grid.SetCurSel(0); } void QSMainDlg::FillProtoCombo() { cmbProto.ResetContent(); cmbProto.AddString(TranslateT("All")); for (auto &it : Accounts()) cmbProto.AddString(it->tszAccountName, (LPARAM)it); cmbProto.SetCurSel(0); } int QSMainDlg::FindItem(CRowItem *pRow) { if (pRow == nullptr) return -1; LV_FINDINFO fi = {}; fi.flags = LVFI_PARAM; fi.lParam = LPARAM(pRow); return m_grid.FindItem(-1, &fi); } int QSMainDlg::FindMeta(MCONTACT hMeta, WPARAM &metaNum) { for (auto &it : m_rows) { if (it->hContact != hMeta) continue; // new meta if (it->wparam == 0) { it->wparam = ++hLastMeta; it->lparam = 0; } metaNum = it->wparam; it->lparam++; return it->lparam; } return 0; } CRowItem* QSMainDlg::FindRow(MCONTACT hContact) { for (auto &it : m_rows) if (it->hContact == hContact) return it; return nullptr; } MCONTACT QSMainDlg::GetFocusedContact() { int idx = m_grid.GetSelectionMark(); if (idx == -1) return -1; INT_PTR data = m_grid.GetItemData(idx); return (data == -1) ? -1 : ((CRowItem *)data)->hContact; } int QSMainDlg::GetLVSubItem(int x, int y) { LV_HITTESTINFO info = {}; info.pt.x = x; info.pt.y = y; ScreenToClient(m_grid.GetHwnd(), &info.pt); if (m_grid.SubItemHitTest(&info) == -1) return -1; return (info.flags & LVHT_ONITEM) ? info.iSubItem : -1; } void QSMainDlg::PrepareTable(bool bReset) { m_grid.DeleteAllItems(); HDITEM hdi = {}; hdi.mask = HDI_FORMAT; int old = tableColumns; tableColumns = 0; LV_COLUMN lvc = {}; lvc.mask = LVCF_TEXT | LVCF_WIDTH; for (auto &it : g_plugin.m_columns) { if (it->bEnabled) AddColumn(tableColumns++, it); it->SetSpecialColumns(); } if (bReset) for (int i = old - 1; i >= tableColumns; i--) m_grid.DeleteColumn(i); } bool QSMainDlg::PrepareToFill() { if (g_plugin.m_columns.getCount() == 0) return false; for (auto &it : g_plugin.m_columns) if (it->bEnabled) it->bInit = true; hLastMeta = 0; m_rows.destroy(); for (auto &hContact : Contacts()) m_rows.insert(new CRowItem(hContact, this)); return m_rows.getCount() != 0; } void QSMainDlg::ProcessLine(CRowItem *pRow, bool test) { if (pRow->bDeleted) return; if (test) pRow->bPattern = CheckPattern(pRow); if (pRow->bPattern) { if (!pRow->bActive) { if ((g_plugin.m_flags & QSO_SHOWOFFLINE) || pRow->status != ID_STATUS_OFFLINE) { // check for proto in combo if (!szFilterProto || !mir_strcmp(szFilterProto, pRow->szProto)) { pRow->bActive = true; AddContactToList(pRow->hContact, pRow); } } } } else if (pRow->bActive) { pRow->bActive = false; m_grid.DeleteItem(FindItem(pRow)); } } void QSMainDlg::SaveColumnOrder() { int idx = 0, col = 0; for (auto &it : g_plugin.m_columns) { if (it->bEnabled) { it->width = m_grid.GetColumnWidth(col++); g_plugin.SaveColumn(idx, *it); } idx++; } } void QSMainDlg::ShowContactMsgDlg(MCONTACT hContact) { if (hContact) { Clist_ContactDoubleClicked(hContact); if (g_plugin.m_flags & QSO_AUTOCLOSE) Close(); } } ///////////////////////////////////////////////////////////////////////////////////////// // contact menu static INT_PTR ColChangeFunc(void *pThis, WPARAM hContact, LPARAM, LPARAM param) { ((QSMainDlg *)pThis)->ChangeCellValue(hContact, (int)param); return 0; } void QSMainDlg::ChangeCellValue(MCONTACT hContact, int col) { auto &pCol = g_plugin.m_columns[col]; auto *pRow = FindRow(hContact); if (pRow == nullptr) return; const char *szModule = pCol.module; if (szModule == nullptr) szModule = pRow->szProto; auto &pVal = pRow->pValues[col]; CMStringW wszTitle(FORMAT, TranslateT("Editing of column %s"), pCol.title); ENTER_STRING es = {}; es.szModuleName = MODULENAME; es.caption = TranslateT("Enter new cell value"); es.ptszInitVal = pVal.text; if (!EnterString(&es)) return; replaceStrW(pVal.text, es.ptszResult); if (pCol.datatype != QSTS_STRING) pVal.data = _wtoi(pVal.text); switch (pCol.datatype) { case QSTS_BYTE: db_set_b(hContact, szModule, pCol.setting, pVal.data); break; case QSTS_WORD: db_set_w(hContact, szModule, pCol.setting, pVal.data); break; case QSTS_DWORD: case QSTS_SIGNED: case QSTS_HEXNUM: db_set_dw(hContact, szModule, pCol.setting, pVal.data); break; case QSTS_STRING: db_set_ws(hContact, szModule, pCol.setting, pVal.text); break; } UpdateLVCell(FindItem(pRow), col, pVal.text); } void QSMainDlg::ShowContactMenu(MCONTACT hContact, int col) { if (hContact == 0) return; HANDLE srvhandle = 0; bool bDoit = false; if (col >= 0) { if ((col = ListViewToColumn(col)) == -1) return; auto &pCol = g_plugin.m_columns[col]; if (pCol.setting_type == QST_SETTING && pCol.datatype != QSTS_TIMESTAMP) { bDoit = true; srvhandle = CreateServiceFunctionObjParam("QS/Dummy", &ColChangeFunc, this, col); if (mnuhandle == nullptr) { CMenuItem mi(&g_plugin); SET_UID(mi, 0xD384A798, 0x5D4C, 0x48B4, 0xB3, 0xE2, 0x30, 0x04, 0x6E, 0xD6, 0xF4, 0x81); mi.name.a = LPGEN("Change setting through QS"); mi.pszService = "QS/Dummy"; mnuhandle = Menu_AddContactMenuItem(&mi); } else Menu_ModifyItem(mnuhandle, 0, INVALID_HANDLE_VALUE, 0); } } POINT pt; GetCursorPos(&pt); HMENU hMenu = Menu_BuildContactMenu(hContact); if (hMenu) { int iCmd = ::TrackPopupMenu(hMenu, TPM_RETURNCMD, pt.x, pt.y, 0, m_grid.GetHwnd(), 0); if (iCmd) { if (Clist_MenuProcessCommand(iCmd, MPCF_CONTACTMENU, hContact)) { if (g_plugin.m_flags & QSO_AUTOCLOSE) CloseSrWindow(); } } ::DestroyMenu(hMenu); } if (srvhandle) DestroyServiceFunction(srvhandle); if (bDoit) Menu_ShowItem(mnuhandle, false); } ///////////////////////////////////////////////////////////////////////////////////////// // muptiple selection popup menu static HMENU MakeContainerMenu() { HMENU hMenu = CreatePopupMenu(); for (int i = 0;; i++) { char setting[10]; _itoa_s(i, setting, 10); ptrW wszName(db_get_wsa(0, "TAB_ContainersW", setting)); if (wszName != nullptr) AppendMenuW(hMenu, MF_STRING, 300 + i, wszName); else break; } return hMenu; } void QSMainDlg::ShowMultiPopup(int cnt) { HMENU hMenu = CreatePopupMenu(); AppendMenuW(hMenu, MF_DISABLED + MF_STRING, 0, CMStringW(FORMAT, TranslateT("Selected %d contacts"), cnt)); AppendMenuW(hMenu, MF_SEPARATOR, 0, 0); AppendMenuW(hMenu, MF_STRING, 101, TranslateT("&Delete")); AppendMenuW(hMenu, MF_STRING, 102, TranslateT("&Copy")); AppendMenuW(hMenu, MF_STRING, 103, TranslateT("C&onvert to Meta")); HMENU cntmenu = MakeContainerMenu(); AppendMenuW(hMenu, MF_POPUP, UINT_PTR(cntmenu), TranslateT("Attach to &Tab container")); if (HMENU grpmenu = Clist_GroupBuildMenu(400)) AppendMenuW(hMenu, MF_POPUP, UINT_PTR(grpmenu), TranslateT("&Move to Group")); POINT pt; GetCursorPos(&pt); int iRes = TrackPopupMenu(hMenu, TPM_RETURNCMD+TPM_NONOTIFY, pt.x, pt.y, 0, m_hwnd, 0); switch (iRes) { case 101: DeleteByList(); break; case 102: CopyMultiLines(); break; case 103: ConvertToMeta(); break; } if (iRes >= 300 && iRes <= 399) { wchar_t buf[100]; if (iRes == 300) // default container, just delete setting buf[0] = 0; else GetMenuStringW(cntmenu, iRes, buf, _countof(buf), MF_BYCOMMAND); MoveToContainer(buf); } else if (iRes >= 400 && iRes <= 499) MoveToGroup(Clist_GroupGetName(iRes - 400)); } void QSMainDlg::ConvertToMeta() { MCONTACT hMeta = 0; int nCount = m_grid.GetItemCount(); for (int i = 0; i < nCount; i++) { if (!m_grid.GetItemState(i, LVIS_SELECTED)) continue; auto *pRow = GetRow(i); if (MCONTACT tmp = db_mc_getMeta(pRow->hContact)) { if (hMeta == 0) hMeta = tmp; else if (hMeta != tmp) { MessageBoxW(m_hwnd, TranslateT("Some of selected contacts in different metacontacts already"), L"Quick Search", MB_ICONERROR); return; } } } if (hMeta != 0) if (IDYES != MessageBoxW(0, TranslateT("One or more contacts already belong to the same metacontact. Try to convert anyway?"), L"Quick Search", MB_YESNO + MB_ICONWARNING)) return; for (int i = 0; i < nCount; i++) { if (!m_grid.GetItemState(i, LVIS_SELECTED)) continue; auto *pRow = GetRow(i); if (hMeta) db_mc_addToMeta(pRow->hContact, hMeta); else db_mc_convertToMeta(pRow->hContact); } } void QSMainDlg::MoveToContainer(const wchar_t *pwszName) { int grcol = -1; for (auto &it : g_plugin.m_columns) { if (it->isContainer) { if (it->bEnabled) grcol = g_plugin.m_columns.indexOf(&it); else it->bInit = false; } } int nCount = m_grid.GetItemCount(); for (int i = 0; i < nCount; i++) { if (!m_grid.GetItemState(i, LVIS_SELECTED)) continue; auto *pRow = GetRow(i); if (*pwszName == 0) db_unset(pRow->hContact, "Tab_SRMsg", "containerW"); else db_set_ws(pRow->hContact, "Tab_SRMsg", "containerW", pwszName); if (grcol != -1) { auto &pVal = pRow->pValues[grcol]; replaceStrW(pVal.text, (*pwszName) ? pwszName : nullptr); UpdateLVCell(i, grcol, pwszName); } } } void QSMainDlg::MoveToGroup(const wchar_t *pwszName) { int grcol = -1; for (auto &it : g_plugin.m_columns) { if (it->isGroup) { if (it->bEnabled) grcol = g_plugin.m_columns.indexOf(&it); else it->bInit = false; } } int nCount = m_grid.GetItemCount(); for (int i = 0; i < nCount; i++) { if (!m_grid.GetItemState(i, LVIS_SELECTED)) continue; auto *pRow = GetRow(i); Clist_SetGroup(pRow->hContact, pwszName); if (grcol != -1) { auto &pVal = pRow->pValues[grcol]; replaceStrW(pVal.text, pwszName); UpdateLVCell(i, grcol, pwszName); } } } ///////////////////////////////////////////////////////////////////////////////////////// // grid sorting static int CALLBACK CompareItem(LPARAM l1, LPARAM l2, LPARAM type) { bool typ1, typ2; UINT_PTR i1, i2; int result = 0; CRowItem *r1 = (CRowItem *)l1, *r2 = (CRowItem *)l2; if (type == StatusSort) { i1 = r1->status, i2 = r2->status; if (i1 == ID_STATUS_OFFLINE) i1 += 64; if (i2 == ID_STATUS_OFFLINE) i2 += 64; typ1 = typ2 = false; } else { auto &res1 = r1->pValues[type], &res2 = r2->pValues[type]; i1 = res1.data, i2 = res2.data; typ1 = i1 == -1; typ2 = i1 == -1; if (typ1 && typ2) { // two strings if (res1.text == 0 && res2.text == 0) result = 0; else if (res2.text == 0) result = 1; else if (res1.text == 0) result = -1; else result = lstrcmpiW(res1.text, res2.text); } else if (typ1 || typ2) // string & num result = (typ1) ? 1 : -1; } if (!typ1 && !typ2) { // not strings if (i1 > i2) result = 1; else if (i1 < i2) result = -1; else result = 0; } if (g_plugin.m_flags & QSO_SORTASC) result = -result; return result; } void QSMainDlg::Sort() { if (g_plugin.m_sortOrder >= tableColumns) g_plugin.m_sortOrder = StatusSort; m_grid.SortItems(&CompareItem, ListViewToColumn(g_plugin.m_sortOrder)); if (g_plugin.m_sortOrder != StatusSort && (g_plugin.m_flags & QSO_SORTBYSTATUS)) m_grid.SortItems(&CompareItem, StatusSort); } void QSMainDlg::UpdateLVCell(int item, int column, const wchar_t *pwszText) { auto &pCol = g_plugin.m_columns[column]; auto *pRow = GetRow(item); if (pwszText == nullptr) { auto &pVal = pRow->pValues[column]; replaceStrW(pVal.text, 0); pVal.LoadOneItem(pRow->hContact, pCol, this); pwszText = pVal.text; } m_grid.SetItemText(item, ColumnToListView(column), pwszText); if (pCol.bFilter) ProcessLine(pRow, true); if (g_plugin.m_sortOrder == column) Sort(); } void QSMainDlg::UpdateSB() { m_sbdata.destroy(); for (auto &it : m_rows) { if (it->szProto == nullptr) continue; CStatusBarItem tmp(it->szProto, it->flags); auto *pItem = m_sbdata.find(&tmp); if (pItem == nullptr) m_sbdata.insert(pItem = new CStatusBarItem(it->szProto, it->flags)); pItem->total++; if (it->bActive) pItem->found++; if (it->status != ID_STATUS_OFFLINE) { pItem->online++; if (it->bActive) pItem->liston++; } } DrawSB(); }