#include #include #include #include #include #include "debug.h" #include "resource.h" #include "common.h" #include "datalistview.h" #include "episodelistview.h" #include "layout.h" #include "pl.h" /* Looked-up constants. */ int g_bThemes; int g_dpi = 96; /* Cursors. */ HCURSOR g_hcArrow = LoadCursor(NULL, IDC_ARROW); HCURSOR g_hcSizeNs = LoadCursor(NULL, IDC_SIZENS); /* Fonts. */ HFONT g_hfNormal; HFONT g_hfBold; /* Menus. */ HMENU g_hMenuPopup; /* Windows. */ HWND g_hWndFocus; HWND g_hWnd; HWND g_hWndStatus; /* Child window objects. */ DataListView* g_dlv; EpisodeListView* g_elv; /* Layout handlers. */ Dragger g_dragDlv; /* View settings. */ int g_bViewWatched = 1; int g_bViewTVOriginal = 1; char g_currentScreenwriter[64]; static LRESULT CALLBACK CBTProc(int, WPARAM, LPARAM); static LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM); static void HandleMainMenu(HWND, unsigned short); static void HandleContextMenu(HWND, unsigned short); static void WaitFor(const char*, const char*); static INT_PTR CALLBACK AboutDlgProc(HWND, UINT, WPARAM, LPARAM); static void UpdateTheme(); void OnTerminate() noexcept { const wchar_t* what = L"an exception"; wstring_owner why; try { std::rethrow_exception(std::current_exception()); } catch (const term_t& t) { what = L"a Prolog exception"; try { why = PlString(t); } catch (...) {} } catch (const Win32Error& e) { what = L"a Windows error"; try { why = wstring_owner::copy(e.WhatW()); } catch (...) {} } catch (const std::exception& e) { try { why = wstring_owner::from_narrow(e.what()); } catch (...) {} } catch (...) {} wchar_t msg[256] = {0}; if (why) wszf(msg, L"Episode Browser was terminated due to %s: %s", what, why.p); else wszf(msg, L"Episode Browser was terminated due to %s.", what); MessageBox(g_hWnd, msg, L"Fatal Error", MB_ICONERROR); _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_hMenuPopup = require(LoadMenu((HINSTANCE)NULL, MAKEINTRESOURCE(IDR_POPUPMENU))); g_hMenuPopup = require(GetSubMenu(g_hMenuPopup, 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 = g_hcArrow; wc.hbrBackground = (HBRUSH)COLOR_WINDOW; wc.lpszMenuName = MAKEINTRESOURCE(IDR_MENU); wc.lpszClassName = L"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, L"Episode Browser", L"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 wchar_t*)NULL, WS_CHILD|WS_VISIBLE|SBARS_SIZEGRIP, 0, 0, 0, 0, hWnd, (HMENU)IDR_STATUS, hInstance, (void*)NULL)); ShowWindow(hWnd, nCmdShow); /* Populate episode list view. */ Pl("track_episodes","update_tracked_episodes"); g_elv->Update(); g_elv->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 lib = maybe_make(L"User32.dll"); auto GetDpiForWindow = lib->GetProcAddress("GetDpiForWindow")) g_dpi = GetDpiForWindow(g_hWnd); if (auto lib = maybe_make(L"uxtheme.dll"); lib->GetProcAddress("SetWindowTheme")) g_bThemes = 1; if (auto lib = maybe_make(L"User32.dll"); lib->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_dlv = new DataListView(g_hWnd); g_elv = new EpisodeListView(g_hWnd); /* Get saved view settings. */ Pl("cfg","get_view_watched",&g_bViewWatched); Pl("cfg","get_view_tv_original",&g_bViewTVOriginal); char* s; if (Pl("cfg","get_limit_screenwriter",&s)) strcpy_s(g_currentScreenwriter, sizeof(g_currentScreenwriter), s); int dlvHeight = 0; Pl("cfg","get_dlv_height",&dlvHeight); g_dlv->SetHeight(dlvHeight); 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_elv->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_currentScreenwriter[0]? MF_UNCHECKED: MF_CHECKED); break; case WM_CLOSE: DestroyWindow(hWnd); break; case WM_DESTROY: g_elv->SaveFocus(); PostQuitMessage(0); break; case WM_SIZE: SendMessage(g_hWndStatus, WM_SIZE, wParam, lParam); UpdateLayout(LOWORD(lParam), HIWORD(lParam)); break; case WM_GETMINMAXINFO: ((MINMAXINFO*)lParam)->ptMinTrackSize.x = Dpi(220); ((MINMAXINFO*)lParam)->ptMinTrackSize.y = Dpi(220); break; case WM_THEMECHANGED: UpdateTheme(); UpdateLayout(); break; case 0x02E0: /* WM_DPICHANGED */ { const RECT* const r = (RECT*)lParam; /* Update DPI and cached metrics. */ g_dpi = HIWORD(wParam); Metric = GetSystemMetrics(SM_CXVSCROLL); prefer(SetWindowPos(hWnd, (HWND)NULL, r->left, r->top, r->right-r->left, r->bottom-r->top, SWP_NOZORDER|SWP_NOACTIVATE)); UpdateLayout(r->right-r->left, r->bottom-r->top); break; } case WM_ACTIVATE: switch (wParam) { case WA_INACTIVE: g_hWndFocus = GetFocus(); break; case WA_ACTIVE: case WA_CLICKACTIVE: SetFocus(g_hWndFocus); Pl("track_episodes","update_tracked_episodes"); g_elv->Redraw(); } break; case WM_NOTIFY: switch (((NMHDR*)lParam)->idFrom) { case IDC_EPISODELISTVIEW: return g_elv->HandleNotify(lParam); } break; case WM_COMMAND: { const unsigned short command = LOWORD(wParam); switch (ID_GROUP(command)) { case IDG_MENU: HandleMainMenu(hWnd, command); break; case IDG_CTX: HandleContextMenu(hWnd, command); break; } break; } case WM_MENUSELECT: { /* Look up status bar tip for menu command. The tip * strings are stored in arrays, whose indices * correspond to the IDM_ values (see resource.h). */ const wchar_t* vTipMenu[] = { /*IDM_FILE_EXIT*/L"Close Episode Browser.", /*IDM_FILE_REFRESH*/L"Quickly refresh episode list.", /*IDM_FILE_FETCH_DATA*/L"Fetch episode data from the web (may take a few seconds).", /*IDM_FILE_FETCH_SCREENWRITERS*/L"Fetch screenwriters from the web (may take a minute).", /*IDM_FILE_ABOUT*/L"Show information about Episode Browser.", /*IDM_VIEW_WATCHED*/(g_bViewWatched? L"Click to hide watched episodes.": L"Click to show watched episodes."), /*IDM_VIEW_TV_ORIGINAL*/(g_bViewTVOriginal? L"Click to hide TV original episodes.": L"Click to show TV original episodes."), /*IDM_VIEW_OTHERS*/(g_currentScreenwriter? L"Click to hide episodes by other screenwriters.": L"Click to show episodes by other screenwriters.") }; const wchar_t* vTipCtx[] = { /*IDM_WATCH_LOCALLY*/L"Open local copy of episode, if available.", /*IDM_WATCH_ONLINE*/L"Open episode in the web browser.", /*IDM_TOGGLE*/L"Toggle watched/unwatched status.", /*IDM_FORGET*/L"Reset watched/unwatched status.", /*IDM_LOOKUP*/L"Fetch episode data from the web, such as date, source and hint.", /*IDM_WIKI*/L"Show Detective Conan Wiki entry for episode.", /*IDM_RATE0*/L"Remove episode rating.", /*IDM_RATE1*/L"Rate episode 1/10.", /*IDM_RATE2*/L"Rate episode 2/10.", /*IDM_RATE3*/L"Rate episode 3/10.", /*IDM_RATE4*/L"Rate episode 4/10.", /*IDM_RATE5*/L"Rate episode 5/10.", /*IDM_RATE6*/L"Rate episode 6/10.", /*IDM_RATE7*/L"Rate episode 7/10.", /*IDM_RATE8*/L"Rate episode 8/10.", /*IDM_RATE9*/L"Rate episode 9/10.", /*IDM_RATE10*/L"Rate episode 10/10." }; const unsigned short command = LOWORD(wParam); const unsigned short group = ID_GROUP(command); const wchar_t* tip = {0}; if (group) { const wchar_t** const vTip = group==IDG_MENU? vTipMenu: vTipCtx; tip = vTip[ID_INDEX(command)]; } SendMessage(g_hWndStatus, SB_SETTEXT, MAKEWPARAM(0,0), (LPARAM)tip); break; } case WM_LBUTTONDOWN: g_dragDlv.HandleDown(); break; case WM_SETCURSOR: if (!g_dragDlv.HandleMove()) { /* Use default cursor. */ if ((HWND)wParam == hWnd) return DefWindowProc(hWnd, uMsg, wParam, lParam); else return 0; } return 1; default: return DefWindowProc(hWnd, uMsg, wParam, lParam); } return 0; } /* Process main menu commands. */ void HandleMainMenu(const HWND hWnd, unsigned short command) { switch (command) { case IDM_FILE_EXIT: PostMessage(hWnd, WM_CLOSE, 0, 0); break; case IDM_FILE_REFRESH: g_elv->Update(); break; case IDM_FILE_FETCH_DATA: WaitFor("episode_data","update_episode_data"); break; case IDM_FILE_FETCH_SCREENWRITERS: WaitFor("episode_data","update_screenwriters"); 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_elv->Update(); g_elv->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_elv->Update(); g_elv->EnsureFocusVisible(); break; case IDM_VIEW_OTHERS: /* Show/hide other screenwriters. */ if (g_currentScreenwriter[0]) { CheckMenuItem(GetMenu(hWnd), IDM_VIEW_OTHERS, MF_CHECKED); g_currentScreenwriter[0] = 0; } else { const int iEpFocus = ListView_GetNextItem(g_elv->hWnd, -1, LVNI_FOCUSED); if (iEpFocus == -1) break; LVITEM lvi = {LVIF_PARAM, iEpFocus}; if (!ListView_GetItem(g_elv->hWnd, &lvi)) break; char* s; if (!Pl("episode_data","episode_datum",lvi.lParam,"Screenwriter",&s)) break; strcpy_s(g_currentScreenwriter, sizeof(g_currentScreenwriter), s); CheckMenuItem(GetMenu(hWnd), IDM_VIEW_OTHERS, MF_UNCHECKED); } Pl("cfg","set_limit_screenwriter",g_currentScreenwriter); g_elv->Update(); g_elv->EnsureFocusVisible(); break; } } /* Process context menu commands. */ void HandleContextMenu(const HWND, unsigned short command) { int cNotFound = 0; /* Look through selected items, applying the * selected command to each one. */ LVITEM lvi = {LVIF_PARAM, -1}; while (g_elv->FindNextItem(&lvi, LVNI_SELECTED)) { /* Process rate commands. */ if (ID_SUBGROUP(command) == IDG_CTX_RATE) { Pl("episode_data","rate_episode",lvi.lParam,ID_RATING(command)); g_elv->UpdateItem(lvi.iItem, lvi.lParam); continue; } /* Process other commands. */ switch (command) { case IDM_WATCH_LOCALLY: if (!Pl("local_episode","open_episode_locally",lvi.lParam)) cNotFound++; break; case IDM_WATCH_ONLINE: Pl("local_episode","open_episode_online",lvi.lParam); break; case IDM_TOGGLE: Pl("track_episodes","toggle_episode",lvi.lParam); break; case IDM_FORGET: Pl("track_episodes","forget_episode",lvi.lParam); Pl("track_episodes","update_tracked_episodes"); break; case IDM_LOOKUP: Pl("episode_data","retract_episode",lvi.lParam); g_elv->UpdateItem(lvi.iItem, lvi.lParam); g_dlv->ShowEpisode(lvi.lParam); break; case IDM_WIKI: Pl("episode_data","open_episode_wiki",lvi.lParam); break; } } g_elv->Redraw(); if (cNotFound == 1) { EBMessageBox(L"Episode could not be opened locally.", L"Error", MB_ICONWARNING); } else if (cNotFound) { wchar_t msg[64] = {0}; wszf(msg, L"%d episodes could not be opened locally.", cNotFound); EBMessageBox(msg, L"Error", MB_ICONWARNING); } else if (ID_SUBGROUP(command) == IDG_CTX_RATE) { g_elv->Sort(); g_elv->ShowFocus(); } } /* Call Prolog predicate in other thread, if available. */ void WaitFor(const char* mod, const char* pred) { static atom_t aThread; static int bActive; static int iTimer; static wstring_owner activePred; if (bActive) { wchar_t msg[256] = {0}; wszf(msg, L"Another task (%s) is active. " L"Do you want to cancel the existing task and start a new one?", activePred.p); if (EBMessageBox(msg, L"Error", MB_YESNO|MB_ICONWARNING) != IDYES) return; KillTimer(NULL, iTimer); bActive = 0; g_elv->Update(); } /* The timer procedure animates an ellipsis in the status bar * while the thread is running. */ static auto proc = [](HWND, UINT, UINT_PTR, DWORD) -> void { static int i = 0; if (Pl("episode_data","thread_running",aThread)) { i = (i+1)%4; SendMessage(g_hWndStatus, SB_SETTEXT, MAKEWPARAM(1,0), (LPARAM)(i==0? L".": i==1? L"..": i==2? L"...": L"")); } else { KillTimer(NULL, iTimer); i = 0; bActive = 0; g_elv->Update(); } }; Plx(mod,"thread_create",pred,&aThread); SendMessage(g_hWndStatus, SB_SETTEXT, MAKEWPARAM(1,0), (LPARAM)L"."); if (prefer(iTimer = SetTimer(NULL, -1, 500, proc))) { activePred = wstring_owner::from_narrow(pred); bActive = 1; } } /* 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; } /* Try to style application according to current Windows theme. */ void UpdateTheme() { if (!g_bThemes) return; const BOOL bThemeActive = IsThemeActive(); g_dlv->UpdateTheme(bThemeActive); g_elv->UpdateTheme(bThemeActive); }