#include #include #include #include #include #include #include "resource.h" #include "common.h" #include "datalistview.h" #include "episodelistview.h" #include "main.h" #include "pl.h" /* Application state. */ atom_t g_aThread; int g_bThread; /* Looked-up constants. */ int g_bThemes; int g_iDPI = 96; /* Fonts. */ HFONT g_hfNormal; HFONT g_hfBold; /* Menus. */ HMENU g_hPopupMenu; /* Windows. */ HWND g_hFocus; HWND g_hWnd; HWND g_hWndStatus; /* Child window objects. */ DataListView* g_pDlv; EpisodeListView* g_pElv; /* View settings. */ int g_bViewWatched = 1; int g_bViewTVOriginal = 1; char g_szLimitScreenwriter[64]; static LRESULT CALLBACK CBTProc(int, WPARAM, LPARAM); static LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM); static INT_PTR CALLBACK AboutDlgProc(HWND, UINT, WPARAM, LPARAM); static void UpdateTheme(); template void TerminateMsg(const T* sz1, const T* sz2) noexcept { std::basic_ostringstream ss; ss << AWTEXT(T, "Episode Browser was terminated due to "); ss << sz1; if (sz2) ss << ": " << sz2; else ss << "."; AWFUN(T, MessageBox)(g_hWnd, ss.str().c_str(), AWTEXT(T, "Fatal Error"), MB_ICONERROR); } void OnTerminate() noexcept { try { std::rethrow_exception(std::current_exception()); } catch (const term_t& t) { TCHAR* tsz; if (PL_get_tchars(t, &tsz, CVT_WRITE)) TerminateMsg(TEXT("a Prolog exception"), tsz); else TerminateMsg("a Prolog exception", NULL); } catch (Win32Error& e) { TerminateMsg(TEXT("a Windows error"), e.what()); } catch (std::exception& e) { TerminateMsg("an exception", e.what()); } catch (...) { TerminateMsg("an exception", NULL); } _Exit(1); } int WINAPI WinMain(const HINSTANCE hInstance, const HINSTANCE, char* const, const int nCmdShow) { /* Exit gracefully on uncaught exception. */ std::set_terminate(OnTerminate); /* Initialize Prolog. */ char* argv[] = { (char*)"EpisodeBrowser", NULL }; if (!PL_initialise(1, argv)) throw std::runtime_error("Could not initialize Prolog."); if (!Pl("track_episodes","attach") || !Pl("episode_data","attach")) throw std::runtime_error("Could not attach databases."); /* Initialize common controls, load menu and register window class. */ INITCOMMONCONTROLSEX icc; icc.dwSize = sizeof(icc); icc.dwICC = ICC_WIN95_CLASSES; require(InitCommonControlsEx(&icc)); g_hPopupMenu = require(LoadMenu((HINSTANCE)NULL, MAKEINTRESOURCE(IDR_POPUPMENU))); g_hPopupMenu = require(GetSubMenu(g_hPopupMenu, 0)); WNDCLASSEX wc; memset(&wc, 0, sizeof(WNDCLASSEX)); wc.cbSize = sizeof(WNDCLASSEX); wc.lpfnWndProc = WndProc; wc.hInstance = hInstance; wc.hIcon = LoadIcon(NULL, IDI_APPLICATION); wc.hCursor = LoadCursor(NULL, IDC_ARROW); wc.hbrBackground = (HBRUSH)COLOR_WINDOW; wc.lpszMenuName = MAKEINTRESOURCE(IDR_MENU); wc.lpszClassName = TEXT("Episode Browser"); wc.hIconSm = LoadIcon(NULL, IDI_APPLICATION); require(RegisterClassEx(&wc)); /* Create window. A CBT hook is used to initialize important * global variables before any messages are sent to the new * window. It is vital that the hook is set up correctly. */ const HHOOK hHook = require(SetWindowsHookEx(WH_CBT, CBTProc, (HINSTANCE)NULL, GetCurrentThreadId())); const HWND hWnd = require(CreateWindowEx( 0, TEXT("Episode Browser"), TEXT("Episode Browser"), WS_OVERLAPPEDWINDOW|WS_CLIPCHILDREN, CW_USEDEFAULT, CW_USEDEFAULT, 0, 0, (HWND)NULL, (HMENU)NULL, hInstance, (void*)NULL)); require(UnhookWindowsHookEx(hHook)); g_hWndStatus = require(CreateWindowEx( 0, STATUSCLASSNAME, (const TCHAR*)NULL, WS_CHILD|WS_VISIBLE|SBARS_SIZEGRIP, 0, 0, 0, 0, hWnd, (HMENU)ID_STATUS, hInstance, (void*)NULL)); ShowWindow(hWnd, nCmdShow); /* Populate episode list view. */ Pl("track_episodes","update_tracked_episodes"); g_pElv->Update(); g_pElv->RestoreFocus(); MSG msg; while (GetMessage(&msg, NULL, 0, 0) > 0) { if (IsDialogMessage(hWnd, &msg)) continue; TranslateMessage(&msg); DispatchMessage(&msg); } PL_halt(0); return 0; } static LRESULT CALLBACK CBTProc(const int nCode, const WPARAM wParam, const LPARAM lParam) { if (nCode < 0 || nCode != HCBT_CREATEWND || g_hWnd) return CallNextHookEx(0, nCode, wParam, lParam); /* This code is run ONCE, at the creation of the top-level * window -- before WndProc! This is important, as it * initializes global variables that are used by WndProc. */ g_hWnd = (HWND)wParam; /* Look up constants. */ if (auto opLib = maybe_make(TEXT("User32.dll"))) if (auto GetDpiForWindow = opLib->GetProcAddress("GetDpiForWindow")) g_iDPI = GetDpiForWindow(g_hWnd); if (auto opLib = maybe_make(TEXT("uxtheme.dll"))) if (opLib->GetProcAddress("SetWindowTheme")) g_bThemes = 1; if (auto opLib = maybe_make(TEXT("User32.dll"))) { if (opLib->GetProcAddress("SystemParametersInfo" WA)) { NONCLIENTMETRICS m; m.cbSize = sizeof(NONCLIENTMETRICS); require(SystemParametersInfo(SPI_GETNONCLIENTMETRICS, sizeof(NONCLIENTMETRICS), &m, 0)); g_hfNormal = require(CreateFontIndirect(&m.lfMessageFont)); } } else g_hfNormal = static_cast(require(GetStockObject(DEFAULT_GUI_FONT))); LOGFONT lf; require(GetObject(g_hfNormal, sizeof(LOGFONT), &lf)); lf.lfWeight = FW_BOLD; g_hfBold = require(CreateFontIndirect(&lf)); /* Create child windows. */ g_pDlv = new DataListView(g_hWnd); g_pElv = new EpisodeListView(g_hWnd); /* Get saved view settings. */ char* sz; Pl("cfg","get_view_watched",&g_bViewWatched); Pl("cfg","get_view_tv_original",&g_bViewTVOriginal); if (Pl("cfg","get_limit_screenwriter",&sz)) strcpy_s(g_szLimitScreenwriter, sizeof(g_szLimitScreenwriter), sz); return 0; } LRESULT CALLBACK WndProc(const HWND hWnd, const UINT uMsg, const WPARAM wParam, LPARAM lParam) { switch (uMsg) { case WM_CREATE: UpdateTheme(); SetWindowPos(hWnd, NULL, -1, -1, Dpi(510), Dpi(412), SWP_NOZORDER|SWP_NOMOVE|SWP_NOACTIVATE); SetFocus(g_pElv->hWnd); /* Set menu item checkmarks according to saved settings. */ CheckMenuItem(GetMenu(hWnd), IDM_VIEW_WATCHED, g_bViewWatched? MF_CHECKED: MF_UNCHECKED); CheckMenuItem(GetMenu(hWnd), IDM_VIEW_TV_ORIGINAL, g_bViewTVOriginal? MF_CHECKED: MF_UNCHECKED); CheckMenuItem(GetMenu(hWnd), IDM_VIEW_OTHERS, g_szLimitScreenwriter[0]? MF_UNCHECKED: MF_CHECKED); break; case WM_CLOSE: DestroyWindow(hWnd); break; case WM_DESTROY: g_pElv->SaveFocus(); PostQuitMessage(0); break; case WM_SIZE: SendMessage(g_hWndStatus, WM_SIZE, wParam, lParam); UpdateLayout(LOWORD(lParam), HIWORD(lParam)); break; case WM_GETMINMAXINFO: { MINMAXINFO* const pMMI = (MINMAXINFO*)lParam; pMMI->ptMinTrackSize.x = Dpi(220); pMMI->ptMinTrackSize.y = Dpi(220); break; } case WM_THEMECHANGED: UpdateTheme(); break; case 0x02E0: /* WM_DPICHANGED */ { const RECT* const lpr = (RECT*)lParam; g_iDPI = HIWORD(wParam); prefer(SetWindowPos(hWnd, (HWND)NULL, lpr->left, lpr->top, lpr->right-lpr->left, lpr->bottom-lpr->top, SWP_NOZORDER|SWP_NOACTIVATE)); UpdateLayout(lpr->right-lpr->left, lpr->bottom-lpr->top); break; } case WM_ACTIVATE: switch (wParam) { case WA_INACTIVE: g_hFocus = GetFocus(); break; case WA_ACTIVE: case WA_CLICKACTIVE: SetFocus(g_hFocus); Pl("track_episodes","update_tracked_episodes"); g_pElv->Redraw(); } break; case WM_NOTIFY: switch (((NMHDR*)lParam)->idFrom) { case IDC_EPISODELISTVIEW: return g_pElv->HandleNotify(lParam); } break; case WM_TIMER: switch (wParam) { case IDT_TIMER: { static int i = 0; /* Animate ellipsis in status bar while doing * work in other thread. */ if (Pl("episode_data","thread_running",g_aThread)) { i = (i+1)%4; SendMessage(g_hWndStatus, SB_SETTEXT, MAKEWPARAM(1,0), (LPARAM)(i==0? TEXT("."): i==1? TEXT(".."): i==2? TEXT("..."): TEXT(""))); } else { i = 0; g_bThread = 0; prefer(KillTimer(hWnd, IDT_TIMER)); g_pElv->Update(); } break; } } break; case WM_COMMAND: switch (LOWORD(wParam)) { case IDM_FILE_EXIT: PostMessage(hWnd, WM_CLOSE, 0, 0); break; case IDM_FILE_REFRESH: g_pElv->Update(); break; case IDM_FILE_FETCH_DATA: if (g_bThread) break; Pl("episode_data","thread_create","update_episode_data",&g_aThread); goto t; case IDM_FILE_FETCH_SCREENWRITERS: if (g_bThread) break; Pl("episode_data","thread_create","update_screenwriters",&g_aThread); t: KillTimer(hWnd, IDT_TIMER); if (!prefer(SetTimer(hWnd, IDT_TIMER, 500, (TIMERPROC)NULL))) break; SendMessage(g_hWndStatus, SB_SETTEXT, MAKEWPARAM(1,0), (LPARAM)TEXT(".")); g_bThread = 1; break; case IDM_FILE_ABOUT: DialogBox( GetModuleHandle(NULL), MAKEINTRESOURCE(IDD_ABOUT), hWnd, AboutDlgProc ); break; case IDM_VIEW_WATCHED: CheckMenuItem(GetMenu(hWnd), IDM_VIEW_WATCHED, g_bViewWatched? MF_UNCHECKED: MF_CHECKED); g_bViewWatched = !g_bViewWatched; Pl("cfg","set_view_watched",g_bViewWatched); g_pElv->Update(); g_pElv->EnsureFocusVisible(); break; case IDM_VIEW_TV_ORIGINAL: CheckMenuItem(GetMenu(hWnd), IDM_VIEW_TV_ORIGINAL, g_bViewTVOriginal? MF_UNCHECKED: MF_CHECKED); g_bViewTVOriginal = !g_bViewTVOriginal; Pl("cfg","set_view_tv_original",g_bViewTVOriginal); g_pElv->Update(); g_pElv->EnsureFocusVisible(); break; case IDM_VIEW_OTHERS: /* Show/hide other screenwriters. */ if (g_szLimitScreenwriter[0]) { CheckMenuItem(GetMenu(hWnd), IDM_VIEW_OTHERS, MF_CHECKED); g_szLimitScreenwriter[0] = 0; } else { const int iEpFocus = ListView_GetNextItem(g_pElv->hWnd, -1, LVNI_FOCUSED); if (iEpFocus == -1) break; LVITEM lvi; lvi.iItem = iEpFocus; lvi.mask = LVIF_PARAM; if (!ListView_GetItem(g_pElv->hWnd, &lvi)) break; char* sz; if (!Pl("episode_data","episode_datum",lvi.lParam,"Screenwriter",&sz)) break; strcpy_s(g_szLimitScreenwriter, sizeof(g_szLimitScreenwriter), sz); CheckMenuItem(GetMenu(hWnd), IDM_VIEW_OTHERS, MF_UNCHECKED); } Pl("cfg","set_limit_screenwriter",g_szLimitScreenwriter); g_pElv->Update(); g_pElv->EnsureFocusVisible(); break; case IDM_WATCH_LOCALLY: case IDM_WATCH_ONLINE: case IDM_TOGGLE: case IDM_FORGET: case IDM_LOOKUP: case IDM_WIKI: case IDM_RATE10: case IDM_RATE9: case IDM_RATE8: case IDM_RATE7: case IDM_RATE6: case IDM_RATE5: case IDM_RATE4: case IDM_RATE3: case IDM_RATE2: case IDM_RATE1: case IDM_RATE0: { int iRating; LVITEM lvi; /* Look through selected items, applying the * selected command to each one. */ lvi.mask = LVIF_PARAM; lvi.iItem = -1; while ((lvi.iItem = ListView_GetNextItem( g_pElv->hWnd, lvi.iItem, LVNI_SELECTED)) != -1) { if (!ListView_GetItem(g_pElv->hWnd, &lvi)) goto b; switch (LOWORD(wParam)) { case IDM_WATCH_LOCALLY: if (!Pl("local_episode","open_episode_locally",lvi.lParam)) EBMessageBox(TEXT("Local episode could not be opened."), TEXT("Error"), MB_ICONWARNING); break; case IDM_WATCH_ONLINE: Pl("local_episode","open_episode_online",lvi.lParam); break; case IDM_TOGGLE: Pl("track_episodes","toggle_episode",lvi.lParam); g_pElv->Redraw(); break; case IDM_FORGET: Pl("track_episodes","forget_episode",lvi.lParam); Pl("track_episodes","update_tracked_episodes"); g_pElv->Redraw(); break; case IDM_LOOKUP: Pl("episode_data","retract_episode",lvi.lParam); g_pElv->UpdateItem(&lvi); g_pElv->Redraw(); g_pDlv->ShowEpisode(lvi.lParam); break; case IDM_WIKI: Pl("episode_data","open_episode_wiki",lvi.lParam); break; case IDM_RATE10: iRating = 10; goto r; case IDM_RATE9: iRating = 9; goto r; case IDM_RATE8: iRating = 8; goto r; case IDM_RATE7: iRating = 7; goto r; case IDM_RATE6: iRating = 6; goto r; case IDM_RATE5: iRating = 5; goto r; case IDM_RATE4: iRating = 4; goto r; case IDM_RATE3: iRating = 3; goto r; case IDM_RATE2: iRating = 2; goto r; case IDM_RATE1: iRating = 1; goto r; case IDM_RATE0: iRating = 0; r: Pl("episode_data","rate_episode",lvi.lParam,iRating); g_pElv->UpdateItem(&lvi); break; } } switch (LOWORD(wParam)) { case IDM_RATE10: case IDM_RATE9: case IDM_RATE8: case IDM_RATE7: case IDM_RATE6: case IDM_RATE5: case IDM_RATE4: case IDM_RATE3: case IDM_RATE2: case IDM_RATE1: case IDM_RATE0: g_pElv->Redraw(); g_pElv->Sort(); g_pElv->ShowFocus(); } b: break; } } break; default: return DefWindowProc(hWnd, uMsg, wParam, lParam); } return 0; } /* Handle messages to Help > About dialog. */ INT_PTR CALLBACK AboutDlgProc(const HWND hWnd, const UINT uMsg, const WPARAM wParam, const LPARAM) { switch (uMsg) { case WM_CLOSE: EndDialog(hWnd, IDOK); break; case WM_COMMAND: switch (LOWORD(wParam)) { case IDOK: EndDialog(hWnd, IDOK); break; } break; default: return FALSE; } return TRUE; } void UpdateLayout(int w, int h) { if (!g_hWndStatus) return; RECT rc, rrStatus; if (w && h) rc = {0, 0, w, h}; else require(GetClientRect(g_hWnd, &rc)); require(GetRelativeRect(g_hWndStatus, &rrStatus)); SendMessage(g_hWnd, WM_SETREDRAW, FALSE, 0); /* Resize list views. */ const long pad = IsThemeActive()? Dpi(6): 0; /* Add padding in modern themes. */ const long cyDlv = rrStatus.top-g_pDlv->Height()-pad; require(SetWindowRect(g_pDlv->hWnd, pad, cyDlv, rc.right-pad, rrStatus.top-pad)); require(SetWindowRect(g_pElv->hWnd, pad, pad, rc.right-pad, cyDlv-pad)); g_pDlv->ResizeColumns(rc.right-pad-pad); g_pElv->ResizeColumns(rc.right-pad-pad); /* Resize status bar parts. */ const int aParts[] = {rc.right-Dpi(55), rc.right}; SendMessage(g_hWndStatus, SB_SETPARTS, (WPARAM)sizeof(aParts), (LPARAM)aParts); SendMessage(g_hWnd, WM_SETREDRAW, TRUE, 0); RedrawWindow(g_hWnd, NULL, NULL, RDW_ERASE|RDW_FRAME|RDW_INVALIDATE|RDW_ALLCHILDREN); } /* Try to style application according to current Windows theme. */ void UpdateTheme() { if (!g_bThemes) return; const BOOL bThemeActive = IsThemeActive(); g_pDlv->UpdateTheme(bThemeActive); g_pElv->UpdateTheme(bThemeActive); }