// Included inside namespace ks by ui.cpp; shares the native control/layout helpers. namespace { constexpr int LogChoice = 601, LogRows = 602, LogRefresh = 603, LogStatus = 604, LogCopy = 605, LogClear = 606, LogProgram = 607, LogCommand = 608, LogResult = 609, LogProblems = 610, LogDetails = 611, LogReset = 612; struct LogViewState { std::vector rows; std::vector visible; int sortColumn = 0; bool descending = true, rendering = false; }; LogViewState *logState(HWND w) { return reinterpret_cast(GetPropW(w, L"LogViewState")); } const std::wstring &logCell(const OperationLogRow &row, int column) { switch (column) { case 0: return row.time; case 1: return row.process; case 2: return row.command; case 3: return row.source; case 4: return row.target; default: return row.outcome; } } void showLogDetails(HWND w) { auto *state = logState(w); if (!state || state->rendering) return; int selected = ListView_GetNextItem(GetDlgItem(w, LogRows), -1, LVNI_SELECTED); value(w, LogDetails, selected >= 0 && (size_t)selected < state->visible.size() ? state->rows[state->visible[selected]].details : L"Select an operation to inspect its text and stages."); } void renderLogRows(HWND w, App &app) { auto *state = logState(w); if (!state) return; state->rendering = true; state->visible.clear(); auto matches = [&](int id, const std::wstring &field) { return SendDlgItemMessageW(w, id, CB_GETCURSEL, 0, 0) <= 0 || value(w, id) == field; }; for (size_t i = 0; i < state->rows.size(); ++i) { const auto &row = state->rows[i]; if (matches(LogProgram, row.process) && matches(LogCommand, row.command) && matches(LogResult, row.outcome) && (!checked(w, LogProblems) || row.problem)) state->visible.push_back(i); } std::stable_sort(state->visible.begin(), state->visible.end(), [&](size_t a, size_t b) { const auto &left = logCell(state->rows[a], state->sortColumn); const auto &right = logCell(state->rows[b], state->sortColumn); if (left == right) return state->descending ? a > b : a < b; int comparison = CompareStringOrdinal(left.c_str(), -1, right.c_str(), -1, TRUE); return state->descending ? comparison == CSTR_GREATER_THAN : comparison == CSTR_LESS_THAN; }); HWND list = GetDlgItem(w, LogRows); SendMessageW(list, WM_SETREDRAW, FALSE, 0); ListView_DeleteAllItems(list); for (size_t i = 0; i < state->visible.size(); ++i) { const auto &row = state->rows[state->visible[i]]; LVITEMW item{}; item.mask = LVIF_TEXT; item.iItem = (int)i; item.pszText = const_cast(row.time.c_str()); ListView_InsertItem(list, &item); for (int column = 1; column < 6; ++column) ListView_SetItemText(list, (int)i, column, const_cast(logCell(row, column).c_str())); } HWND header = ListView_GetHeader(list); for (int i = 0; i < 6; ++i) { HDITEMW item{}; item.mask = HDI_FORMAT; Header_GetItem(header, i, &item); item.fmt &= ~(HDF_SORTUP | HDF_SORTDOWN); if (i == state->sortColumn) item.fmt |= state->descending ? HDF_SORTDOWN : HDF_SORTUP; Header_SetItem(header, i, &item); } SendMessageW(list, WM_SETREDRAW, TRUE, 0); InvalidateRect(list, nullptr, TRUE); state->rendering = false; value(w, LogStatus, std::to_wstring(state->visible.size()) + L" of " + std::to_wstring(state->rows.size()) + L" operations / events. " + (app.settings.writeLog ? L"Diagnostic logging enabled." : L"Logging disabled; saved entries only.")); if (!state->visible.empty()) ListView_SetItemState(list, 0, LVIS_SELECTED | LVIS_FOCUSED, LVIS_SELECTED | LVIS_FOCUSED); showLogDetails(w); if (state->visible.empty()) value(w, LogDetails, state->rows.empty() ? L"No recorded operations. Use a conversion shortcut with logging enabled, then refresh." : L"No operations match these filters. Reset filters to show all entries."); } void fillLogFilter(HWND w, int id, const std::set &choices) { auto previous = value(w, id); HWND combo = GetDlgItem(w, id); SendMessageW(combo, CB_RESETCONTENT, 0, 0); SendMessageW(combo, CB_ADDSTRING, 0, (LPARAM)L"All"); int selected = 0, index = 1; for (const auto &choice : choices) { if (choice.empty()) continue; SendMessageW(combo, CB_ADDSTRING, 0, (LPARAM)choice.c_str()); if (choice == previous) selected = index; ++index; } SendMessageW(combo, CB_SETCURSEL, selected, 0); } void refreshLogView(HWND w, App &app) { auto *state = logState(w); if (!state) return; try { auto contents = readDiagnosticLog(SendDlgItemMessageW(w, LogChoice, CB_GETCURSEL, 0, 0) == 1); state->rows = parseOperationLog(contents); if (contents.starts_with(L"No log yet.") || contents.starts_with(L"No previous log") || contents == L"The log is empty.") state->rows.clear(); std::set programs, commands, results; for (const auto &row : state->rows) { programs.insert(row.process); commands.insert(row.command); results.insert(row.outcome); } fillLogFilter(w, LogProgram, programs); fillLogFilter(w, LogCommand, commands); fillLogFilter(w, LogResult, results); renderLogRows(w, app); } catch (const std::exception &e) { value(w, LogStatus, L"Could not read the log: " + wide(e.what())); } } void copyLogRows(HWND w, App &app) { auto *state = logState(w); if (!state) return; std::wstring copied; HWND list = GetDlgItem(w, LogRows); for (int row = ListView_GetNextItem(list, -1, LVNI_SELECTED); row >= 0; row = ListView_GetNextItem(list, row, LVNI_SELECTED)) { if ((size_t)row >= state->visible.size()) continue; const auto &entry = state->rows[state->visible[row]]; copied += entry.time + L"\t" + entry.process + L"\t" + entry.command + L"\t" + entry.source + L" -> " + entry.target + L"\t" + entry.outcome + L"\r\n" + entry.details + L"\r\n\r\n"; } if (!copied.empty()) { auto clip = textClip(copied); clip.formats.push_back({0, L"ExcludeClipboardContentFromMonitorProcessing", {1, 0, 0, 0}}); app.ownSequence = writeClipboard(app.window, clip); } } LRESULT CALLBACK logProc(HWND w, UINT msg, WPARAM wp, LPARAM lp) { auto *app = reinterpret_cast(GetWindowLongPtrW(w, GWLP_USERDATA)); if (msg == WM_NCCREATE) { SetWindowLongPtrW(w, GWLP_USERDATA, (LONG_PTR)((CREATESTRUCTW *)lp)->lpCreateParams); SetPropW(w, L"LogViewState", new LogViewState); return TRUE; } if (!app) return DefWindowProcW(w, msg, wp, lp); switch (msg) { case WM_COMMAND: if ((LOWORD(wp) == LogRefresh && HIWORD(wp) == BN_CLICKED) || (LOWORD(wp) == LogChoice && HIWORD(wp) == CBN_SELCHANGE)) { refreshLogView(w, *app); return 0; } if ((LOWORD(wp) >= LogProgram && LOWORD(wp) <= LogResult && HIWORD(wp) == CBN_SELCHANGE) || (LOWORD(wp) == LogProblems && HIWORD(wp) == BN_CLICKED)) { renderLogRows(w, *app); return 0; } if (LOWORD(wp) == LogReset && HIWORD(wp) == BN_CLICKED) { for (int id : {LogProgram, LogCommand, LogResult}) SendDlgItemMessageW(w, id, CB_SETCURSEL, 0, 0); CheckDlgButton(w, LogProblems, BST_UNCHECKED); renderLogRows(w, *app); return 0; } if (LOWORD(wp) == LogCopy && HIWORD(wp) == BN_CLICKED) { try { copyLogRows(w, *app); } catch (const std::exception &e) { error(w, e); } return 0; } if (LOWORD(wp) == LogClear && HIWORD(wp) == BN_CLICKED) { try { app->clearLog(SendDlgItemMessageW(w, LogChoice, CB_GETCURSEL, 0, 0) == 1); refreshLogView(w, *app); } catch (const std::exception &e) { error(w, e); } return 0; } if (LOWORD(wp) != IDCANCEL || HIWORD(wp) != BN_CLICKED) break; [[fallthrough]]; case WM_CLOSE: DestroyWindow(w); return 0; case WM_NOTIFY: if (((NMHDR *)lp)->idFrom != LogRows) break; if (((NMHDR *)lp)->code == LVN_ITEMCHANGED) showLogDetails(w); if (((NMHDR *)lp)->code == LVN_COLUMNCLICK) { auto *state = logState(w); int column = ((NMLISTVIEW *)lp)->iSubItem; state->descending = state->sortColumn == column ? !state->descending : false; state->sortColumn = column; renderLogRows(w, *app); } if (((NMHDR *)lp)->code == LVN_KEYDOWN) { auto key = ((NMLVKEYDOWN *)lp)->wVKey; if (GetKeyState(VK_CONTROL) & 0x8000) { if (key == 'A') ListView_SetItemState(GetDlgItem(w, LogRows), -1, LVIS_SELECTED, LVIS_SELECTED); if (key == 'C') { try { copyLogRows(w, *app); } catch (const std::exception &e) { error(w, e); } } } } return 0; case WM_DPICHANGED: resizeForDpi(w, wp, lp); return 0; case WM_NCDESTROY: delete logState(w); RemovePropW(w, L"LogViewState"); app->logWindow = nullptr; disposeFont(w); break; } return DefWindowProcW(w, msg, wp, lp); } } // namespace void showLogs(App &app) { if (app.logWindow) { showRequestedWindow(app.logWindow); activateWindow(app.logWindow); return; } auto w = makeWindow(app, L"KSwitcher.Logs", L"KSwitcher diagnostic operations", logProc, 1100, 760); app.logWindow = w; auto choice = control(w, L"COMBOBOX", L"", WS_TABSTOP | CBS_DROPDOWNLIST, LogChoice, 16, 16, 280, 140); SendMessageW(choice, CB_ADDSTRING, 0, (LPARAM)L"Current log — kswitcher.log"); SendMessageW(choice, CB_ADDSTRING, 0, (LPARAM)L"Previous log — kswitcher.log.1"); SendMessageW(choice, CB_SETCURSEL, 0, 0); button(w, L"&Refresh", LogRefresh, 310, 14, 100); button(w, L"&Copy selected", LogCopy, 424, 14, 140); button(w, L"Clear &log", LogClear, 578, 14, 100); control(w, L"STATIC", L"", 0, LogStatus, 16, 49, 1068, 22); label(w, L"&Program", 16, 78, 170); control(w, L"COMBOBOX", L"", WS_TABSTOP | CBS_DROPDOWNLIST | WS_VSCROLL, LogProgram, 16, 101, 174, 300); label(w, L"Co&mmand", 206, 78, 200); control(w, L"COMBOBOX", L"", WS_TABSTOP | CBS_DROPDOWNLIST | WS_VSCROLL, LogCommand, 206, 101, 204, 300); label(w, L"Res&ult", 426, 78, 300); control(w, L"COMBOBOX", L"", WS_TABSTOP | CBS_DROPDOWNLIST | WS_VSCROLL, LogResult, 426, 101, 310, 300); checkbox(w, L"&Only problems", LogProblems, 752, 102, 166); button(w, L"Reset &filters", LogReset, 938, 98, 146); INITCOMMONCONTROLSEX common{sizeof(common), ICC_LISTVIEW_CLASSES}; InitCommonControlsEx(&common); auto list = control(w, WC_LISTVIEWW, L"", WS_TABSTOP | WS_BORDER | LVS_REPORT | LVS_SHOWSELALWAYS, LogRows, 16, 142, 1068, 260); ListView_SetExtendedListViewStyle(list, LVS_EX_FULLROWSELECT | LVS_EX_GRIDLINES | LVS_EX_DOUBLEBUFFER | LVS_EX_INFOTIP); const wchar_t *titles[] = {L"Time", L"Program", L"Command", L"Source layout", L"Target layout", L"Result"}; const int widths[] = {165, 145, 150, 130, 130, 320}; for (int i = 0; i < 6; ++i) { LVCOLUMNW column{}; column.mask = LVCF_TEXT | LVCF_WIDTH; column.pszText = const_cast(titles[i]); column.cx = scale(w, widths[i]); ListView_InsertColumn(list, i, &column); } auto details = edit(w, LogDetails, 16, 416, 1068, 276, ES_MULTILINE | ES_AUTOVSCROLL | ES_READONLY | WS_VSCROLL | WS_BORDER); auto hint = control( w, L"STATIC", L"Click a column to sort. Select an operation for details. Ctrl+C copies selected operations.", 0, 0, 16, 712, 860, 22); button(w, L"Clos&e", IDCANCEL, 974, 708, 110); auto *layout = windowLayoutState(w); layout->stretch = list; layout->details = details; layout->bottomLeft = hint; layout->bottomRight = GetDlgItem(w, IDCANCEL); arrangeWindow(w); refreshLogView(w, app); showRequestedWindow(w); activateWindow(w); }