aboutsummaryrefslogtreecommitdiff
path: root/c/episodelistview.cpp
diff options
context:
space:
mode:
authorJohn Ankarström <john@ankarstrom.se>2022-07-10 23:23:09 +0200
committerJohn Ankarström <john@ankarstrom.se>2022-07-10 23:25:09 +0200
commit295d423cc47f9ee8a72134dc544892a03b279311 (patch)
tree3e89b0bbcf42b3053225eb0dff88b887dd16df48 /c/episodelistview.cpp
parent85a4ad2c184ed915915a2fb630415a80ed9a286f (diff)
downloadEpisodeBrowser-295d423cc47f9ee8a72134dc544892a03b279311.tar.gz
Convert to C++.
I already hit upon some object-oriented programming patterns in *listview.c, so I felt that it would be natural to use this as an opportunity to learn C++.
Diffstat (limited to 'c/episodelistview.cpp')
-rw-r--r--c/episodelistview.cpp460
1 files changed, 460 insertions, 0 deletions
diff --git a/c/episodelistview.cpp b/c/episodelistview.cpp
new file mode 100644
index 0000000..8fb3ec7
--- /dev/null
+++ b/c/episodelistview.cpp
@@ -0,0 +1,460 @@
+#include <windows.h>
+#include <commctrl.h>
+#include <TCHAR.H>
+#include <SWI-Prolog.h>
+
+#include "resource.h"
+#include "defs.h"
+
+extern DataListView g_dlv;
+
+HWND HSortLv;
+
+int ISort;
+LVITEM LviElvFocus; /* Focused episode. */
+static int CALLBACK ElvSort(LPARAM, LPARAM, LPARAM);
+
+void
+EpisodeListView::Create()
+{
+ LVCOLUMN lvc;
+
+ ListView::Create((HMENU)IDC_EPISODELISTVIEW, 0);
+
+ lvc.mask = LVCF_WIDTH|LVCF_TEXT|LVCF_SUBITEM;
+ lvc.iSubItem = ELVSIEPISODE;
+ lvc.pszText = TEXT("#");
+ lvc.cx = Dpi(42);
+ ListView_InsertColumn(m_hWnd, ELVSIEPISODE, &lvc);
+
+ lvc.iSubItem = ELVSITITLE;
+ lvc.pszText = TEXT("Title");
+ lvc.cx = 500;
+ ListView_InsertColumn(m_hWnd, ELVSITITLE, &lvc);
+
+ lvc.iSubItem = ELVSIRATING;
+ lvc.pszText = TEXT("/");
+ lvc.cx = Dpi(30);
+ ListView_InsertColumn(m_hWnd, ELVSIRATING, &lvc);
+
+ if (!Pl("cfg","get_sort","i",&ISort))
+ ISort = 1;
+}
+
+void
+EpisodeListView::DoSort()
+{
+ HSortLv = m_hWnd;
+ ListView_SortItemsEx(m_hWnd, ElvSort, ISort);
+}
+
+LRESULT
+EpisodeListView::HandleNotify(LPARAM lParam)
+{
+ LPNMLISTVIEW lpNmLv;
+ lpNmLv = (LPNMLISTVIEW)lParam;
+
+ switch (lpNmLv->hdr.code) {
+ case LVN_ITEMCHANGED: /* Select/focus episode. */
+ if ((lpNmLv->uChanged & LVIF_STATE) &&
+ (lpNmLv->uNewState & LVIS_FOCUSED)) {
+ LviElvFocus.iItem = lpNmLv->iItem;
+ LviElvFocus.lParam = lpNmLv->lParam;
+ UpdateItem(&LviElvFocus);
+ g_dlv.ShowEpisode(lpNmLv->lParam);
+ }
+ break;
+ case LVN_COLUMNCLICK: /* Sort by column. */
+ {
+ int iColumn;
+ iColumn = lpNmLv->iSubItem+1;
+ ISort = abs(ISort) == iColumn? -ISort: iColumn;
+ Pl("cfg","set_sort","I",ISort);
+ DoSort();
+ ShowFocus();
+ break;
+ }
+ case LVN_KEYDOWN: /* Navigate episodes by keyboard. */
+ {
+ LPNMLVKEYDOWN lpNmLvKd;
+ lpNmLvKd = (LPNMLVKEYDOWN)lParam;
+ switch (lpNmLvKd->wVKey) {
+ case VK_LEFT:
+ SelectUnwatched(-1);
+ break;
+ case VK_RIGHT:
+ SelectUnwatched(1);
+ break;
+ }
+ break;
+ }
+ case NM_CUSTOMDRAW: /* Make unwatched episodes bold. */
+ {
+ LPNMLVCUSTOMDRAW lpLvCd;
+ lpLvCd = (LPNMLVCUSTOMDRAW)lParam;
+ switch (lpLvCd->nmcd.dwDrawStage) {
+ case CDDS_PREPAINT:
+ return CDRF_NOTIFYITEMDRAW;
+ break;
+ case CDDS_ITEMPREPAINT:
+ {
+ extern HFONT g_fBold;
+ if (!Pl("track_episodes","watched","I",lpLvCd->nmcd.lItemlParam)) {
+ SelectObject(lpLvCd->nmcd.hdc, g_fBold);
+ return CDRF_NEWFONT;
+ }
+ break;
+ }
+ }
+ break;
+ }
+ case NM_DBLCLK: /* Open clicked episode. */
+ {
+ if (!Pl("local_episodes","open_episode_locally","I",
+ LviElvFocus.lParam))
+ Pl("local_episodes","open_episode_online","I",
+ LviElvFocus.lParam);
+ break;
+ }
+ case NM_RETURN: /* Open all selected episodes. */
+ {
+ LVITEM lvi;
+ lvi.mask = LVIF_PARAM;
+ lvi.iItem = -1;
+ while ((lvi.iItem = ListView_GetNextItem(
+ m_hWnd, lvi.iItem, LVNI_SELECTED)) != -1) {
+ if (!ListView_GetItem(m_hWnd, &lvi)) goto b;
+ if (!Pl("local_episodes","open_episode_locally","I",
+ lvi.lParam))
+ Pl("local_episodes","open_episode_online","I",
+ lvi.lParam);
+ }
+ b: break;
+ }
+ case NM_RCLICK:
+ {
+ DWORD dwPos;
+ extern HWND g_hWnd;
+ extern HMENU g_hPopupMenu;
+ dwPos = GetMessagePos();
+ TrackPopupMenu(g_hPopupMenu, TPM_RIGHTBUTTON,
+ LOWORD(dwPos), HIWORD(dwPos), 0,
+ g_hWnd, NULL);
+ break;
+ }
+ }
+
+ return 0;
+}
+
+void
+EpisodeListView::Redraw()
+{
+ RedrawWindow(m_hWnd, NULL, NULL,
+ RDW_ERASE|RDW_FRAME|RDW_INVALIDATE|RDW_ALLCHILDREN);
+}
+
+void
+EpisodeListView::SetTop(int iItem)
+{
+ int iLast;
+ iLast = ListView_GetItemCount(m_hWnd)-1;
+ ListView_EnsureVisible(m_hWnd, iLast, TRUE);
+ ListView_EnsureVisible(m_hWnd, iItem, TRUE);
+}
+
+/* Select previously focused episode. */
+void
+EpisodeListView::SelectFocus()
+{
+ int i, iEpisode, iItem;
+ LVFINDINFO lvfi;
+
+ iItem = 0;
+ if (!Pl("cfg","get_focus","i",&iEpisode)) return;
+
+ lvfi.flags = LVFI_PARAM;
+ lvfi.lParam = iEpisode;
+ i = 0;
+ while ((iItem = ListView_FindItem(m_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(m_hWnd, -1, &lvfi)) == -1 && i++ < 100)
+ lvfi.lParam = --iEpisode;
+ if (iItem != -1) goto s;
+ return;
+
+s: ListView_SetItemState(m_hWnd, -1, LVIF_STATE, LVIS_SELECTED);
+ SetTop(iItem > 5? iItem-5: 0);
+ ListView_SetItemState(m_hWnd, iItem,
+ LVIS_SELECTED|LVIS_FOCUSED, LVIS_SELECTED|LVIS_FOCUSED);
+ LviElvFocus.iItem = iItem;
+ LviElvFocus.lParam = iEpisode;
+ UpdateItem(&LviElvFocus);
+ g_dlv.ShowEpisode(iEpisode);
+}
+
+/* Select next/previous unwatched episode. */
+void
+EpisodeListView::SelectUnwatched(int iDir)
+{
+ int i, iEpNew, iItemNew;
+ LVFINDINFO lvfi;
+ LVITEM lviFocus;
+
+ /* Get focused episode. */
+ lviFocus.mask = LVIF_PARAM;
+ if ((lviFocus.iItem = ListView_GetNextItem(m_hWnd, -1, LVNI_FOCUSED)) != -1
+ && ListView_GetItem(m_hWnd, &lviFocus))
+ ;
+ else
+ return;
+
+ i = 0;
+ lvfi.flags = LVFI_PARAM;
+ lvfi.lParam = lviFocus.lParam;
+
+ do {
+ if (!Pl("track_episodes",iDir > 0? "next_unwatched": "previous_unwatched",
+ "Ii",lvfi.lParam,&iEpNew))
+ return;
+
+ lvfi.lParam = iEpNew;
+ if ((iItemNew = ListView_FindItem(m_hWnd, -1, &lvfi)) != -1) {
+ ListView_SetItemState(m_hWnd,-1,LVIF_STATE,LVIS_SELECTED);
+ ListView_SetSelectionMark(m_hWnd, iItemNew);
+ ListView_SetItemState(m_hWnd, iItemNew,
+ LVIS_SELECTED|LVIS_FOCUSED,
+ LVIS_SELECTED|LVIS_FOCUSED);
+ Redraw();
+ ListView_EnsureVisible(m_hWnd, iItemNew, TRUE);
+ return;
+ }
+ } while (i++ < 1000);
+}
+
+void
+EpisodeListView::ShowFocus()
+{
+ int iEpFocus;
+ iEpFocus = ListView_GetNextItem(m_hWnd, -1, LVNI_FOCUSED);
+ if (iEpFocus == -1) return;
+ ListView_EnsureVisible(m_hWnd, iEpFocus, TRUE);
+}
+
+/* Update episode list. */
+void
+EpisodeListView::Update()
+{
+ int cEp, i, iEp, iEpFocus, iItem, iItemMark, iItemTopNew;
+ LVITEM lvi, lviEpisode, lviTop;
+ LVFINDINFO lvfi;
+ extern HWND g_hWndStatus;
+ static TCHAR tszDisp[16], tszEpisode[16];
+ static int aEpSel[2048];
+
+ lviEpisode.mask = LVIF_TEXT|LVIF_PARAM;
+
+ /* Save scrolling position. */
+ lviTop.iItem = ListView_GetTopIndex(m_hWnd);
+ lviTop.mask = LVIF_PARAM;
+ ListView_GetItem(m_hWnd, &lviTop);
+
+ /* Save selected episodes. */
+ i = 0;
+ lvi.mask = LVIF_PARAM;
+ lvi.iItem = -1;
+ while ((lvi.iItem = ListView_GetNextItem(
+ m_hWnd, lvi.iItem, LVNI_SELECTED)) != -1
+ && i < 2048)
+ if (ListView_GetItem(m_hWnd, &lvi))
+ aEpSel[i++] = lvi.lParam;
+ aEpSel[i] = 0;
+ iItemMark = ListView_GetSelectionMark(m_hWnd);
+
+ /* Save focus. */
+ iEpFocus = 0;
+ if ((lvi.iItem = ListView_GetNextItem(m_hWnd, -1, LVNI_FOCUSED)) != -1
+ && ListView_GetItem(m_hWnd, &lvi))
+ iEpFocus = lvi.lParam;
+
+ SendMessage(m_hWnd, WM_SETREDRAW, FALSE, 0);
+ ListView_DeleteAllItems(m_hWnd);
+
+ if (!Pl("episode_data","ensure_episode_data","")) return;
+ if (!Pl("episode_data","episode_count","i",&cEp)) return;
+
+ for (iEp = 1, iItem = 0; iEp <= cEp; iEp++) {
+ extern char g_szLimitScreenwriter[];
+ extern int g_bViewTVOriginal, g_bViewWatched;
+
+ if (g_szLimitScreenwriter[0])
+ if (!Pl("episode_data","episode_datum","ISS",
+ iEp,"Screenwriter",g_szLimitScreenwriter))
+ continue;
+
+ if (!g_bViewWatched)
+ if (Pl("track_episodes","watched","I",iEp)) continue;
+
+ if (!g_bViewTVOriginal)
+ if (Pl("episode_data","tv_original","I",iEp)) continue;
+
+ /* Format episode number string. */
+ _stprintf_s(tszEpisode, sizeof(tszEpisode), TEXT("%d"), iEp);
+
+ /* Insert item. */
+ lviEpisode.iItem = iItem++;
+ lviEpisode.iSubItem = ELVSIEPISODE;
+ lviEpisode.pszText = tszEpisode;
+ lviEpisode.lParam = iEp;
+ ListView_InsertItem(m_hWnd, &lviEpisode);
+ UpdateItem(&lviEpisode);
+ }
+
+ DoSort();
+
+ lvfi.flags = LVFI_PARAM;
+
+ /* Reset selection. */
+ for (i = 0; aEpSel[i]; i++) {
+ int iItemSel;
+ lvfi.lParam = aEpSel[i];
+ if ((iItemSel = ListView_FindItem(m_hWnd, -1, &lvfi)) != -1)
+ ListView_SetItemState(m_hWnd, iItemSel,
+ LVIS_SELECTED, LVIS_SELECTED);
+ }
+ if (iItemMark != -1)
+ ListView_SetSelectionMark(m_hWnd, iItemMark);
+
+ /* Reset focus. */
+ if (iEpFocus) {
+ int iItemFocus;
+ i = 0;
+ do
+ lvfi.lParam = iEpFocus+i;
+ while ((iItemFocus = ListView_FindItem(m_hWnd, -1, &lvfi)) == -1
+ && i++ < 100);
+ if (iItemFocus != -1)
+ ListView_SetItemState(m_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. */
+ i = 0;
+ do
+ lvfi.lParam = lviTop.lParam+i;
+ while ((iItemTopNew = ListView_FindItem(m_hWnd, -1, &lvfi)) == -1
+ && i++ < 100);
+ if (iItemTopNew != -1)
+ SetTop(iItemTopNew);
+
+ _stprintf_s(tszDisp, sizeof(tszDisp), TEXT("%d"), iItem);
+ SendMessage(g_hWndStatus, SB_SETTEXT, MAKEWPARAM(1,0), (LPARAM)tszDisp);
+ SendMessage(m_hWnd, WM_SETREDRAW, TRUE, 0);
+}
+
+/* Update episode name and rating. */
+void
+EpisodeListView::UpdateItem(LPLVITEM lpLvi)
+{
+ char *szName;
+ int iRating;
+ TCHAR *tszName;
+ static TCHAR tszRating[3];
+
+ tszName = NULL;
+ if (!Pl("episode_data","episode_title","Is",lpLvi->lParam,&szName)) {
+ if (!Pl("episode_data","update_episode_data","")) goto r;
+ if (!Pl("episode_data","episode_title","Is",lpLvi->lParam,&szName))
+ goto r;
+ }
+ tszName = TszFromSz(szName, CP_UTF8);
+ if (tszName)
+ ListView_SetItemText(m_hWnd, lpLvi->iItem, ELVSITITLE, tszName);
+
+r: if (!Pl("episode_data","episode_rating","Ii",lpLvi->lParam,&iRating)) {
+ ListView_SetItemText(m_hWnd, lpLvi->iItem, ELVSIRATING, TEXT(""));
+ goto f;
+ }
+
+ _stprintf_s(tszRating, sizeof(tszRating), TEXT("%d"), iRating);
+ ListView_SetItemText(m_hWnd, lpLvi->iItem, ELVSIRATING, tszRating);
+
+f: if (tszName) free(tszName);
+}
+
+/* Sort list view items, iSort being the 1-based index of the column
+ * to sort by. If iSort is negative, the order is descending. */
+int CALLBACK
+ElvSort(LPARAM iItem1, LPARAM iItem2, LPARAM iSort)
+{
+ int iOrder;
+ LVITEM lvi1, lvi2;
+ lvi1.mask = lvi2.mask = LVIF_PARAM;
+ lvi1.iItem = iItem1; lvi2.iItem = iItem2;
+ if (!ListView_GetItem(HSortLv, &lvi1)) return 0;
+ if (!ListView_GetItem(HSortLv, &lvi2)) return 0;
+ iOrder = Cmp(iSort, 0);
+ switch (abs(iSort)-1) {
+ case ELVSIEPISODE:
+ return iOrder*Cmp(lvi1.lParam, lvi2.lParam);
+ break;
+ case ELVSIRATING:
+ {
+ int iRating1, iRating2;
+ iRating1 = iSort > 0? 99: -1;
+ iRating2 = iSort > 0? 99: -1;
+ Pl("episode_data","episode_rating","Ii",lvi1.lParam,&iRating1);
+ Pl("episode_data","episode_rating","Ii",lvi2.lParam,&iRating2);
+ if (iRating1 == iRating2)
+ return Cmp(lvi1.lParam, lvi2.lParam);
+ return iOrder*Cmp(iRating1, iRating2);
+ break;
+ }
+ case ELVSITITLE:
+ {
+ char *sz1, *sz2;
+ int cch, cch1, cch2;
+ if (!Pl("episode_data","episode_title","Is",lvi1.lParam,&sz1))
+ return 0;
+ if (!Pl("episode_data","episode_title","Is",lvi2.lParam,&sz2))
+ return 0;
+ cch1 = strlen(sz1);
+ cch2 = strlen(sz2);
+ cch = cch1 > cch2? cch2: cch1;
+ return iOrder*_strnicmp(sz1, sz2, cch);
+ break;
+ }
+ default:
+ return 0;
+ }
+}
+
+LRESULT CALLBACK
+EpisodeListView::WndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
+{
+ switch (uMsg) {
+ case WM_GETDLGCODE:
+ {
+ LRESULT lResult;
+
+ /* 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 ElvHandleNotify. */
+
+ lResult = CallWindowProc(m_prevProc, hWnd, uMsg, wParam, lParam);
+ if (lParam && ((MSG *)lParam)->message == WM_KEYDOWN
+ && ((MSG *)lParam)->wParam == VK_RETURN)
+ return DLGC_WANTMESSAGE;
+ return lResult;
+ }
+ }
+
+ return ListView::WndProc(hWnd, uMsg, wParam, lParam);
+}