#include #include #include #include #include "resource.h" #include "data.h" #include "datalistview.h" #include "episodelistview.h" #include "ext.h" #include "listview.h" #include "main.h" #include "util.h" #include "win.h" EpisodeListView::EpisodeListView(Window& parent) : ListView(parent, reinterpret_cast(IDC_EPISODELISTVIEW), 0) { LVCOLUMN lvc; lvc.mask = LVCF_WIDTH|LVCF_TEXT|LVCF_SUBITEM; lvc.iSubItem = ELVSIEPISODE; lvc.pszText = const_cast(L"#"); lvc.cx = Dpi(42); ListView_InsertColumn(hWnd, ELVSIEPISODE, &lvc); lvc.iSubItem = ELVSITITLE; lvc.pszText = const_cast(L"Title"); lvc.cx = 500; ListView_InsertColumn(hWnd, ELVSITITLE, &lvc); lvc.iSubItem = ELVSIRATING; lvc.pszText = const_cast(L"/"); lvc.cx = Dpi(30); ListView_InsertColumn(hWnd, ELVSIRATING, &lvc); /* Get saved sort-by-column setting. */ m_iSortCol = parent.cfg.iSortCol; } void EpisodeListView::EnsureFocusVisible() { const int iEpFocus = ListView_GetNextItem(hWnd, -1, LVNI_FOCUSED); if (iEpFocus != -1) ListView_EnsureVisible(hWnd, iEpFocus, TRUE); } void EpisodeListView::HandleContextMenu(const WORD command) { int cNotFound = 0; /* Look through selected items, applying the * selected command to each one. */ LVITEM lvi = {LVIF_PARAM, -1}; while (FindNextItem(&lvi, LVNI_SELECTED)) { ElvDataA& e = parent.fvElv.At(lvi.lParam-1); if (ID_SUBGROUP(command) == IDG_CTX_RATE) { /* Process rate commands. */ if (e.rating = ID_RATING(command)) Swprintf(e.sRating, L"%d", e.rating); else e.sRating[0] = 0; } else { /* Process other commands. */ switch (command) { case IDM_WATCH_LOCALLY: if(!OpenLocally(lvi.lParam)) cNotFound++; break; case IDM_WATCH_ONLINE: OpenOnline(lvi.lParam); break; case IDM_TOGGLE: e.bWatched = !e.bWatched; break; case IDM_WIKI: OpenWiki(lvi.lParam); break; } } } Redraw(); if (ID_SUBGROUP(command) == IDG_CTX_RATE) { /* If ratings changed, the episodes may need to be resorted. */ Sort(); ShowFocus(); } else if (cNotFound) { /* Show warning if local episodes were not found. */ if (cNotFound == 1) EBMessageBox(L"Episode could not be opened locally.", L"Error", MB_ICONWARNING); else { wchar_t msg[64] = {0}; Swprintf(msg, L"%d episodes could not be opened locally.", cNotFound); EBMessageBox(msg, L"Error", MB_ICONWARNING); } } } LRESULT EpisodeListView::HandleNotify(const LPARAM lParam) { const NMLISTVIEW* const nm = reinterpret_cast(lParam); switch (nm->hdr.code) { case LVN_GETDISPINFO: /* Display item. */ { NMLVDISPINFO* const nm = reinterpret_cast(lParam); ElvDataA& e = parent.fvElv.At(nm->item.lParam-1); wchar_t* vs[] = {e.siEp, e.title, e.sRating}; /* ELVSIEPISODE, ELVSITITLE, ELVSIRATING */ nm->item.pszText = vs[nm->item.iSubItem]; return 0; } case LVN_ITEMCHANGED: /* Select/focus episode. */ if ((nm->uChanged & LVIF_STATE) && (nm->uNewState & LVIS_FOCUSED)) { parent.dlv.ShowEpisode(nm->lParam); } return 0; case LVN_COLUMNCLICK: /* Sort by column. */ { const int iCol = nm->iSubItem+1; /* The sign of m_iSortCol decides the sort order. */ m_iSortCol = abs(m_iSortCol) == iCol? -m_iSortCol: iCol; parent.cfg.iSortCol = m_iSortCol; Sort(); ShowFocus(); return 0; } case LVN_KEYDOWN: /* Navigate episodes by keyboard. */ { const NMLVKEYDOWN *const nm = reinterpret_cast(lParam); switch (nm->wVKey) { case VK_LEFT: SelectUnwatched(-1); break; case VK_RIGHT: SelectUnwatched(1); break; } return 0; } case NM_CUSTOMDRAW: /* Make unwatched episodes bold. */ { const NMLVCUSTOMDRAW* const nm = reinterpret_cast(lParam); switch (nm->nmcd.dwDrawStage) { case CDDS_PREPAINT: return CDRF_NOTIFYITEMDRAW; break; case CDDS_ITEMPREPAINT: { const ElvDataA& e = parent.fvElv.At(nm->nmcd.lItemlParam-1); if (!e.bWatched) { extern HFONT g_hfBold; Require(SelectObject(nm->nmcd.hdc, g_hfBold)); return CDRF_NEWFONT; } break; } } return 0; } case NM_DBLCLK: /* Open clicked episode. */ case NM_RETURN: /* Open all selected episodes. */ { LVITEM lvi = {LVIF_PARAM, -1}; while (FindNextItem(&lvi, LVNI_SELECTED)) OpenLocally(lvi.lParam) || OpenOnline(lvi.lParam); return 0; } case NM_RCLICK: { extern HMENU g_hMenuPopup; const DWORD pos = GetMessagePos(); Require(TrackPopupMenu(g_hMenuPopup, TPM_RIGHTBUTTON, LOWORD(pos), HIWORD(pos), 0, parent.hWnd, nullptr)); return 0; } default: return 0; } } void EpisodeListView::Redraw() { RedrawWindow(hWnd, nullptr, nullptr, RDW_ERASE|RDW_FRAME|RDW_INVALIDATE|RDW_ALLCHILDREN); } void EpisodeListView::ResizeColumns(int w) { ListView_SetColumnWidth(hWnd, ELVSIEPISODE, LVSCW_AUTOSIZE); int cx = ListView_GetColumnWidth(hWnd, ELVSIEPISODE)+Dpi(4); ListView_SetColumnWidth(hWnd, ELVSIEPISODE, cx); cx += ListView_GetColumnWidth(hWnd, ELVSIRATING); ListView_SetColumnWidth(hWnd, ELVSITITLE, w-cx-Metric-Dpi(4)); } /* Select previously focused episode. */ void EpisodeListView::RestoreFocus() { int i, iEpisode, iItem; LVFINDINFO lvfi; iItem = 0; iEpisode = parent.cfg.iFocus; lvfi.flags = LVFI_PARAM; lvfi.lParam = iEpisode; i = 0; while ((iItem = ListView_FindItem(hWnd, -1, &lvfi)) == -1 && i++ < 100) lvfi.lParam = ++iEpisode; if (iItem != -1) goto s; iEpisode -= 100; lvfi.lParam = iEpisode; i = 0; while ((iItem = ListView_FindItem(hWnd, -1, &lvfi)) == -1 && i++ < 100) lvfi.lParam = --iEpisode; if (iItem != -1) goto s; return; s: parent.dlv.ShowEpisode(iEpisode); ListView_SetItemState(hWnd, -1, LVIF_STATE, LVIS_SELECTED); SetTop(iItem > 5? iItem-5: 0); ListView_SetItemState(hWnd, iItem, LVIS_SELECTED|LVIS_FOCUSED, LVIS_SELECTED|LVIS_FOCUSED); } void EpisodeListView::SaveFocus() { LVITEM lvi = {LVIF_PARAM, -1}; if (FindNextItem(&lvi, LVNI_FOCUSED)) parent.cfg.iFocus = lvi.lParam; } void EpisodeListView::SetTop(const int iItem) { const int iItemLast = ListView_GetItemCount(hWnd)-1; ListView_EnsureVisible(hWnd, iItemLast, TRUE); ListView_EnsureVisible(hWnd, iItem, TRUE); } void EpisodeListView::SelectUnwatched(int dir) { dir = dir > 0? 1: -1; /* Get focused episode. */ LVITEM lviFocus = {LVIF_PARAM, -1}; if (!FindNextItem(&lviFocus, LVNI_FOCUSED)) return; LVFINDINFO lvfi; int i, iEpNew, iItemNew; i = 0; lvfi.flags = LVFI_PARAM; lvfi.lParam = lviFocus.lParam; do { /* Loop through episodes to find next/previous * unwatched episode. */ iEpNew = lvfi.lParam; for (;;) { iEpNew += dir; if (iEpNew == 0 || static_cast(iEpNew) > parent.fvElv.c || !parent.fvElv[iEpNew-1].siEp[0]) return; if (!parent.fvElv[iEpNew-1].bWatched) break; } /* Select the episode, if it is in the list view. If * not, try again a thousand times. */ lvfi.lParam = iEpNew; if ((iItemNew = ListView_FindItem(hWnd, -1, &lvfi)) != -1) { ListView_SetItemState(hWnd, -1, LVIF_STATE, LVIS_SELECTED); ListView_SetSelectionMark(hWnd, iItemNew); ListView_SetItemState(hWnd, iItemNew, LVIS_SELECTED|LVIS_FOCUSED, LVIS_SELECTED|LVIS_FOCUSED); Redraw(); ListView_EnsureVisible(hWnd, iItemNew, TRUE); return; } } while (i++ < 1000); } void EpisodeListView::ShowFocus() { const int iEpFocus = ListView_GetNextItem(hWnd, -1, LVNI_FOCUSED); if (iEpFocus == -1) return; ListView_EnsureVisible(hWnd, iEpFocus, TRUE); } void EpisodeListView::Sort() { ListView_SortItemsEx(hWnd, EpisodeListView::SortProc, reinterpret_cast(this)); } int CALLBACK EpisodeListView::SortProc(const LPARAM iItem1, const LPARAM iItem2, const LPARAM extra) { EpisodeListView* const elv = reinterpret_cast(extra); LVITEM lvi1 = {LVIF_PARAM, static_cast(iItem1)}; if (!ListView_GetItem(elv->hWnd, &lvi1)) return 0; LVITEM lvi2 = {LVIF_PARAM, static_cast(iItem2)}; if (!ListView_GetItem(elv->hWnd, &lvi2)) return 0; /* abs(m_iSortCol) is the 1-based index of the column to sort by. * If m_iSortCol is negative, the order is descending. */ const int order = Cmp(elv->m_iSortCol, 0); const ElvDataA& e1 = elv->parent.fvElv[lvi1.lParam-1]; const ElvDataA& e2 = elv->parent.fvElv.At(lvi2.lParam-1); switch (abs(elv->m_iSortCol)-1) { case ELVSIEPISODE: return order*Cmp(lvi1.lParam, lvi2.lParam); case ELVSIRATING: { int rating1 = e1.rating; int rating2 = e2.rating; if (!rating1) rating1 = elv->m_iSortCol > 0? 99: -1; if (!rating2) rating2 = elv->m_iSortCol > 0? 99: -1; if (rating1 == rating2) return Cmp(lvi1.lParam, lvi2.lParam); return order*Cmp(rating1, rating2); } case ELVSITITLE: return order*_wcsicmp(e1.title, e2.title); default: return 0; } } void EpisodeListView::Update() { /* Save scrolling position. */ int iEpTop = 0; { LVITEM lviTop = {LVIF_PARAM, ListView_GetTopIndex(hWnd)}; ListView_GetItem(hWnd, &lviTop); iEpTop = lviTop.lParam; } /* Save selected episodes. */ int iItemMark; std::vector vEpSel; { LVITEM lvi = {LVIF_PARAM, -1}; while (FindNextItem(&lvi, LVNI_SELECTED)) vEpSel.push_back(lvi.lParam); iItemMark = ListView_GetSelectionMark(hWnd); } /* Save focus. */ int iEpFocus = 0; { LVITEM lvi = {LVIF_PARAM, -1}; if (FindNextItem(&lvi, LVNI_FOCUSED)) iEpFocus = lvi.lParam; } { int cItem = 0; LVITEM lviEpisode = {LVIF_TEXT|LVIF_PARAM}; /* Retrieve episode data and add list view items. */ SendMessageW(hWnd, WM_SETREDRAW, FALSE, 0); ListView_DeleteAllItems(hWnd); for (int iEp = 1; iEp <= parent.cfg.cEp; iEp++) { ElvDataA& e = parent.fvElv.At(iEp-1); if (!e.siEp[0]) continue; if (parent.cfg.limitToScreenwriter[0] && wcscmp(parent.fvDlv.At(iEp-1).screenwriter, parent.cfg.limitToScreenwriter) != 0) continue; if (!parent.cfg.bViewWatched && e.bWatched) continue; if (!parent.cfg.bViewTVOriginal && e.bTVOriginal) continue; /* Insert item. */ lviEpisode.iItem = cItem++; lviEpisode.iSubItem = ELVSIEPISODE; lviEpisode.pszText = LPSTR_TEXTCALLBACK; lviEpisode.lParam = iEp; ListView_InsertItem(hWnd, &lviEpisode); ListView_SetItemText(hWnd, lviEpisode.iItem, ELVSITITLE, LPSTR_TEXTCALLBACK); ListView_SetItemText(hWnd, lviEpisode.iItem, ELVSIRATING, LPSTR_TEXTCALLBACK); } /* Show number of displayed items in status bar. */ wchar_t disp[16]; Swprintf(disp, L"%d", cItem); Status(disp, 1); } Sort(); LVFINDINFO lvfi; lvfi.flags = LVFI_PARAM; /* Reset selection. */ for (const int iEpSel : vEpSel) { int iItemSel; lvfi.lParam = iEpSel; if ((iItemSel = ListView_FindItem(hWnd, -1, &lvfi)) != -1) ListView_SetItemState(hWnd, iItemSel, LVIS_SELECTED, LVIS_SELECTED); } if (iItemMark != -1) ListView_SetSelectionMark(hWnd, iItemMark); /* Reset focus. */ if (iEpFocus) { int iItemFocus; int i = 0; do lvfi.lParam = iEpFocus+i; while ((iItemFocus = ListView_FindItem(hWnd, -1, &lvfi)) == -1 && i++ < 100); if (iItemFocus != -1) ListView_SetItemState(hWnd, iItemFocus, LVIS_FOCUSED, LVIS_FOCUSED); } /* Try to reset scrolling position. Note that this must be * done last, as focusing an item scrolls it into view. */ { int iItemTopNew; int i = 0; do lvfi.lParam = iEpTop+i; while ((iItemTopNew = ListView_FindItem(hWnd, -1, &lvfi)) == -1 && i++ < 100); if (iItemTopNew != -1) SetTop(iItemTopNew); } SendMessageW(hWnd, WM_SETREDRAW, TRUE, 0); } LRESULT CALLBACK EpisodeListView::WndProc(const HWND hWnd, const UINT uMsg, const WPARAM wParam, const LPARAM lParam) { switch (uMsg) { case WM_GETDLGCODE: { /* For the episode list view, the Enter key should not * be handled by the dialog manager, but instead be sent * along to the main window procedure, so that it may be * handled by the NM_RETURN case in HandleNotify. */ const LRESULT lResult = CallWindowProc(m_proc0, hWnd, uMsg, wParam, lParam); MSG* const msg = reinterpret_cast(lParam); if (lParam && msg->message == WM_KEYDOWN && msg->wParam == VK_RETURN) return DLGC_WANTMESSAGE; else return lResult; } } return ListView::WndProc(hWnd, uMsg, wParam, lParam); }