#include #include #include #include #include #include #include "datalistview.h" #include "debug.h" #include "drag.h" #include "episodelistview.h" #include "res.h" #include "test.h" #include "util.h" #include "window.h" #ifdef _DEBUG #define XMAIN 30 #define YMAIN 30 #else #define XMAIN CW_USEDEFAULT #define YMAIN CW_USEDEFAULT #endif /* Looked-up constants. */ int g_dpi = 96; /* Fonts. */ HFONT g_hfNormal; HFONT g_hfBold; /* Cursors. */ HCURSOR g_hcArrow = LoadCursorW(nullptr, IDC_ARROW); HCURSOR g_hcSizeNs = LoadCursorW(nullptr, IDC_SIZENS); /* Main window object. */ Window* g_window; /* Optional Windows functions. */ BOOL (__stdcall *IsThemeActive)(); HRESULT (__stdcall *SetWindowTheme)(HWND, const wchar_t*, const wchar_t*); /* Initialize important global state on parent window creation. */ static void InitializeMainWindow(HWND); /* Process main window commands. */ static LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM); /* Handle messages to Help > About dialog. */ static INT_PTR CALLBACK AboutDlgProc(HWND, UINT, WPARAM, LPARAM); /* Handle messages to File > Preferences... dialog. */ static INT_PTR CALLBACK PreferencesDlgProc(HWND, UINT, WPARAM, LPARAM); int WINAPI WinMain( _In_ const HINSTANCE hInstance, _In_opt_ const HINSTANCE, _In_ char* const, _In_ const int nCmdShow) { std::set_terminate(OnTerminate); setbuf(stdout, nullptr); LIBXML_TEST_VERSION; INITCOMMONCONTROLSEX icc; icc.dwSize = sizeof(icc); icc.dwICC = ICC_WIN95_CLASSES; if (!InitCommonControlsEx(&icc)) return EBMessageBox(Err(WINDOWS, L"Common controls could not be initialized: %s.").what, L"Initialization Error", MB_ICONERROR), 1; if (CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED) != S_OK) return EBMessageBox(L"COM library could not be initialized.", L"Initialization Error", MB_ICONERROR), 1; WNDCLASSEX wc; memset(&wc, 0, sizeof(WNDCLASSEX)); wc.cbSize = sizeof(WNDCLASSEX); wc.lpfnWndProc = Except; wc.hInstance = hInstance; wc.hIcon = LoadIconW(nullptr, IDI_APPLICATION); wc.hCursor = g_hcArrow; wc.hbrBackground = reinterpret_cast(COLOR_WINDOW); wc.lpszMenuName = MAKEINTRESOURCEW(IDR_MENU); wc.lpszClassName = L"Episode Browser"; wc.hIconSm = LoadIconW(nullptr, IDI_APPLICATION); if (!RegisterClassExW(&wc)) return EBMessageBox(Err(WINDOWS, L"Window class could not be registered: %s.").what, L"Initialization Error", MB_ICONERROR), 1; /* InitializeMainWindow is called before the first message is * sent to WndProc. This is important, as it initializes * global state on which WndProc relies. */ WithNextWindow(Noexcept); HWND hWnd; if (!(hWnd = CreateWindowExW( 0, L"Episode Browser", L"Episode Browser", WS_OVERLAPPEDWINDOW|WS_CLIPCHILDREN, XMAIN, YMAIN, 0, 0, nullptr, nullptr, hInstance, nullptr))) return EBMessageBox(Err(WINDOWS, L"Main window could not be created: %s.").what, L"Initialization Error", MB_ICONERROR), 1; if (!(g_window->hWndStatus = CreateWindowExW( 0, STATUSCLASSNAME, nullptr, WS_CHILD|WS_VISIBLE|SBARS_SIZEGRIP, 0, 0, 0, 0, hWnd, reinterpret_cast(IDR_STATUS), hInstance, nullptr))) return EBMessageBox(Err(WINDOWS, L"Status bar could not be created: %s.").what, L"Initialization Error", MB_ICONERROR), 1; ShowWindow(hWnd, nCmdShow); /* Populate episode list view. */ /* TODO: Update tracked episodes. */ g_window->elv.Update(); g_window->elv.RestoreFocus(); #ifdef _DEBUG RunTests(*g_window); #endif MSG msg; while (GetMessageW(&msg, nullptr, 0, 0) > 0) { if (IsDialogMessageW(hWnd, &msg)) continue; TranslateMessage(&msg); DispatchMessageW(&msg); } return 0; } static void InitializeMainWindow(const HWND hWnd) { /* 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. */ /* Look up DPI. */ if (auto lib = Library::Maybe(L"User32.dll"); auto GetDpiForWindow = lib? lib->GetProcAddress("GetDpiForWindow"): nullptr) g_dpi = GetDpiForWindow(hWnd); /* Load normal font. */ if (auto lib = Library::Maybe(L"User32.dll"); lib && lib->GetProcAddress("SystemParametersInfoW")) { NONCLIENTMETRICSW m = {sizeof(NONCLIENTMETRICSW)}; #if (WINVER >= 0x0600) m.cbSize -= 4; #endif if (!SystemParametersInfoW(SPI_GETNONCLIENTMETRICS, sizeof(NONCLIENTMETRICSW), &m, 0)) throw Err(WINDOWS, L"Non-client metrics could not be queried: %s."); if (!(g_hfNormal = CreateFontIndirectW(&m.lfMessageFont))) throw Err(WINDOWS, L"System message font could not be loaded: %s."); } else if (!(g_hfNormal = static_cast(GetStockObject(DEFAULT_GUI_FONT)))) throw Err(WINDOWS, L"System GUI font could not be loaded: %s."); /* Load bold font. */ { LOGFONTW lf; if (!GetObjectW(g_hfNormal, sizeof(LOGFONTW), &lf)) throw Err(WINDOWS, L"Logical system font could not be loaded: %s."); lf.lfWeight = FW_BOLD; if (!(g_hfBold = CreateFontIndirectW(&lf))) throw Err(WINDOWS, L"Bold font could not be loaded: %s."); } /* Load theme functions, if available. */ if (HMODULE hModule = LoadLibraryW(L"uxtheme.dll")) { IsThemeActive = (decltype(IsThemeActive))(void*)GetProcAddress(hModule, "IsThemeActive"); SetWindowTheme = (decltype(SetWindowTheme))(void*)GetProcAddress(hModule, "SetWindowTheme"); } g_window = new Window(hWnd); } LRESULT CALLBACK WndProc(const HWND hWnd, const UINT uMsg, const WPARAM wParam, const LPARAM lParam) { return g_window->WndProc(hWnd, uMsg, wParam, lParam); } INT_PTR CALLBACK AboutDlgProc(const HWND hWnd, const UINT uMsg, const WPARAM wParam, const LPARAM) { switch (uMsg) { case WM_CLOSE: EndDialog(hWnd, IDCANCEL); return TRUE; case WM_COMMAND: if (LOWORD(wParam) == IDOK || LOWORD(wParam) == IDCANCEL) EndDialog(hWnd, LOWORD(wParam)); return TRUE; default: return FALSE; } } INT_PTR CALLBACK PreferencesDlgProc(const HWND hWnd, const UINT uMsg, const WPARAM wParam, const LPARAM) { if (uMsg == WM_CLOSE) { EndDialog(hWnd, IDCANCEL); return TRUE; } else if (uMsg == WM_COMMAND) { switch (LOWORD(wParam)) { case IDOK: case IDCANCEL: EndDialog(hWnd, LOWORD(wParam)); return TRUE; case IDC_BROWSE: { wchar_t path[MAX_PATH]; BROWSEINFOW bi = {hWnd, nullptr, path, L"Episode Browser will look for local episode files in the" L" chosen directory and its subdirectories.", BIF_EDITBOX|BIF_NEWDIALOGSTYLE|BIF_NONEWFOLDERBUTTON}; if (auto pidl = SHBrowseForFolderW(&bi)) { if (!SHGetPathFromIDListW(pidl, path)) throw Err(GENERIC, L"Invalid path selected."); EBMessageBox(path, L"You Chose"); } return TRUE; } } return TRUE; } else return FALSE; } LRESULT CALLBACK Window::WndProc(const HWND hWnd, const UINT uMsg, const WPARAM wParam, const LPARAM lParam) { switch (uMsg) { case WM_CREATE: UpdateTheme(); SetWindowPos(hWnd, nullptr, -1, -1, Dpi(510), Dpi(412), SWP_NOZORDER|SWP_NOMOVE|SWP_NOACTIVATE); SetFocus(elv.hWnd); /* Set menu item checkmarks according to saved settings. */ CheckMenuItem(GetMenu(hWnd), IDM_VIEW_WATCHED, cfg.bViewWatched? MF_CHECKED: MF_UNCHECKED); CheckMenuItem(GetMenu(hWnd), IDM_VIEW_TV_ORIGINAL, cfg.bViewTVOriginal? MF_CHECKED: MF_UNCHECKED); CheckMenuItem(GetMenu(hWnd), IDM_VIEW_OTHERS, cfg.limitToScreenwriter[0]? MF_UNCHECKED: MF_CHECKED); return 0; case WM_CLOSE: DestroyWindow(hWnd); return 0; case WM_DESTROY: elv.SaveFocus(); PostQuitMessage(0); return 0; case WM_SIZE: SendMessage(hWndStatus, WM_SIZE, wParam, lParam); UpdateLayout(LOWORD(lParam), HIWORD(lParam)); return 0; case WM_GETMINMAXINFO: reinterpret_cast(lParam)->ptMinTrackSize.x = Dpi(220); reinterpret_cast(lParam)->ptMinTrackSize.y = Dpi(220); return 0; case WM_THEMECHANGED: UpdateTheme(); UpdateLayout(); return 0; case 0x02E0: /* WM_DPICHANGED */ /* TODO: What does GetSystemMetrics return depending * on the DPI? Do the cached values need to be updated * when the DPI changes? */ /* Get new DPI. */ g_dpi = HIWORD(wParam); /* Get new window position/size. */ { const RECT* const r = reinterpret_cast(lParam); if (!SetWindowPos(hWnd, nullptr, r->left, r->top, r->right-r->left, r->bottom-r->top, SWP_NOZORDER|SWP_NOACTIVATE)) throw Err(WINDOWS, L"."); UpdateLayout(r->right-r->left, r->bottom-r->top); } return 0; case WM_ACTIVATE: if (wParam == WA_INACTIVE) hWndFocus = GetFocus(); else { SetFocus(hWndFocus); /* TODO: Update tracked episodes. */ elv.Redraw(); } return 0; case WM_NOTIFY: switch (reinterpret_cast(lParam)->idFrom) { case IDC_EPISODELISTVIEW: return elv.HandleNotify(lParam); } return 0; case WM_COMMAND: { const WORD command = LOWORD(wParam); switch (ID_GROUP(command)) { case IDG_MENU: HandleMainMenu(hWnd, command); return 0; case IDG_CTX: elv.HandleContextMenu(command); return 0; default: return 0; } } 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 res.h). */ const wchar_t* vTipMenu[] = { /*IDM_FILE_EXIT*/L"Close Episode Browser.", /*IDM_FILE_REFRESH*/L"Quickly refresh episode list.", /*IDM_FILE_PREFERENCES*/L"Configure Episode Browser.", /*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_FETCH_CANCEL*/L"Stop fetching data from the web.", /*IDM_FILE_ABOUT*/L"Show information about Episode Browser.", /*IDM_VIEW_WATCHED*/(cfg.bViewWatched? L"Click to hide watched episodes.": L"Click to show watched episodes."), /*IDM_VIEW_TV_ORIGINAL*/(cfg.bViewTVOriginal? L"Click to hide TV original episodes.": L"Click to show TV original episodes."), /*IDM_VIEW_OTHERS*/(cfg.limitToScreenwriter? 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_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 WORD command = LOWORD(wParam); const WORD 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)]; } Status(tip, 0); return 0; } case WM_LBUTTONDOWN: dragDlv.HandleLButtonDown(); return 0; case WM_SETCURSOR: if (dragDlv.HandleSetCursor()) return 1; else { /* Use default cursor. */ if (reinterpret_cast(wParam) == hWnd) return DefWindowProc(hWnd, uMsg, wParam, lParam); else return 0; } default: return DefWindowProc(hWnd, uMsg, wParam, lParam); } } void Window::HandleMainMenu(const HWND hWnd, const WORD command) { switch (command) { case IDM_FILE_EXIT: PostMessage(hWnd, WM_CLOSE, 0, 0); break; case IDM_FILE_REFRESH: elv.Update(); break; case IDM_FILE_PREFERENCES: DialogBox( GetModuleHandle(nullptr), MAKEINTRESOURCE(IDD_PREFERENCES), hWnd, Except); break; case IDM_FILE_FETCH_DATA: { WaitFor(*this, FetchData); break; } case IDM_FILE_FETCH_SCREENWRITERS: WaitFor(*this, FetchScreenwriters); break; case IDM_FILE_FETCH_CANCEL: WaitFor(*this, nullptr); break; case IDM_FILE_ABOUT: DialogBox( GetModuleHandle(nullptr), MAKEINTRESOURCE(IDD_ABOUT), hWnd, Except); break; case IDM_VIEW_WATCHED: CheckMenuItem(GetMenu(hWnd), IDM_VIEW_WATCHED, cfg.bViewWatched? MF_UNCHECKED: MF_CHECKED); cfg.bViewWatched = !cfg.bViewWatched; elv.Update(); elv.EnsureFocusVisible(); /* TODO: Remember last valid focus. In case of * non-existing focus, use the last valid focus. */ break; case IDM_VIEW_TV_ORIGINAL: CheckMenuItem(GetMenu(hWnd), IDM_VIEW_TV_ORIGINAL, cfg.bViewTVOriginal? MF_UNCHECKED: MF_CHECKED); cfg.bViewTVOriginal = !cfg.bViewTVOriginal; elv.Update(); elv.EnsureFocusVisible(); break; case IDM_VIEW_OTHERS: if (cfg.limitToScreenwriter[0]) { /* Show episodes by all screenwriters. */ CheckMenuItem(GetMenu(hWnd), IDM_VIEW_OTHERS, MF_CHECKED); cfg.limitToScreenwriter[0] = 0; } else { /* Hide episodes by other screenwriters than current. */ LVITEM lvi = {LVIF_PARAM, -1}; if (elv.FindNextItem(&lvi, LVNI_FOCUSED) && fvDlv.At(lvi.lParam-1).screenwriter[0]) { Wcscpy(cfg.limitToScreenwriter, fvDlv.At(lvi.lParam-1).screenwriter); CheckMenuItem(GetMenu(hWnd), IDM_VIEW_OTHERS, MF_UNCHECKED); } } elv.Update(); elv.EnsureFocusVisible(); break; } } void Window::Status(const wchar_t* msg, unsigned short i) noexcept { SendMessage(hWndStatus, SB_SETTEXT, MAKEWPARAM(i, 0), reinterpret_cast(msg)); } void Window::UpdateLayout(int w, int h) { if (!hWndStatus) return; RECT rc, rrStatus; if (w && h) rc = {0, 0, w, h}; else if (!GetClientRect(hWnd, &rc)) throw Err(WINDOWS, L"Window rectangle could not be retrieved: %s."); if (!GetRelativeRect(hWndStatus, &rrStatus)) throw Err(WINDOWS, L"Status bar rectangle could not be retrieved: %s."); SendMessageW(hWnd, WM_SETREDRAW, FALSE, 0); /* Resize list views. */ const long pad = EBIsThemeActive()? Dpi(6): 0; /* Add padding in modern themes. */ const long cyDlv = rrStatus.top-dlv.Height()-pad; if (!SetWindowRect(dlv.hWnd, pad, cyDlv, rc.right-pad, rrStatus.top-pad)) throw Err(WINDOWS, L"Data list view rectangle could not be updated: %s."); if (!SetWindowRect(elv.hWnd, pad, pad, rc.right-pad, cyDlv-pad)) throw Err(WINDOWS, L"Episode list view rectangle could not be updated: %s."); dlv.ResizeColumns(rc.right-pad-pad); elv.ResizeColumns(rc.right-pad-pad); /* Resize status bar parts. */ const int aParts[] = {rc.right-Dpi(55), rc.right}; SendMessageW(hWndStatus, SB_SETPARTS, sizeof(aParts), reinterpret_cast(aParts)); SendMessageW(hWnd, WM_SETREDRAW, TRUE, 0); RedrawWindow(hWnd, nullptr, nullptr, RDW_ERASE|RDW_FRAME|RDW_INVALIDATE|RDW_ALLCHILDREN); } void Window::UpdateTheme() { if (IsThemeActive) { const bool bThemeActive = IsThemeActive(); dlv.UpdateTheme(bThemeActive); elv.UpdateTheme(bThemeActive); } }