Release v0.6.18: verify conversion results and log problems only
This commit is contained in:
+66
-16
@@ -2,6 +2,8 @@
|
||||
#include "../resources/resource.h"
|
||||
#include "ocr.h"
|
||||
#include "configuration.h"
|
||||
#include "diagnostics.h"
|
||||
#include "paste_probe.h"
|
||||
#include "screenshot_notifications.h"
|
||||
#include <algorithm>
|
||||
#include <commctrl.h>
|
||||
@@ -12,6 +14,12 @@ namespace ks
|
||||
App *App::current = nullptr;
|
||||
namespace
|
||||
{
|
||||
const char *transformCommandName(int command)
|
||||
{
|
||||
return command == Toggle ? "Convert / cycle" : command >= Direct ? "Convert / direct" :
|
||||
command == Case ? "Change case" : command == Translit ? "Transliterate" :
|
||||
command == Undo ? "Undo" : "Automatic / snippet";
|
||||
}
|
||||
ULONG_PTR snippetReplayMarker()
|
||||
{
|
||||
static const char marker{};
|
||||
@@ -854,7 +862,18 @@ void App::transform(int command, int snippet)
|
||||
if (busy.exchange(true) && !preDeferred)
|
||||
{
|
||||
if (manual)
|
||||
diagnosticEvent(L"Transform outcome id=" + std::to_wstring(attempt) + L" reason=busy", true);
|
||||
{
|
||||
diagnosticEvent(L"Transform outcome id=" + std::to_wstring(attempt) + L" reason=busy");
|
||||
try
|
||||
{
|
||||
DiagnosticJson record{{"attempt", std::to_string(attempt)}, {"process", utf8(processName(target))},
|
||||
{"command", transformCommandName(command)}, {"source", "Unknown"}, {"target", "Unknown"},
|
||||
{"outcome", "busy"}, {"verification", "not-attempted"}, {"problem", true},
|
||||
{"elapsedMs", GetTickCount64() - requestedAt}};
|
||||
diagnosticOperation(record.dump());
|
||||
}
|
||||
catch (...) {}
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (preDeferred)
|
||||
@@ -893,7 +912,7 @@ void App::transform(int command, int snippet)
|
||||
} reset{busy, window, preDeferred};
|
||||
auto start = GetTickCount64();
|
||||
std::wstring reason = L"cancelled-before-selection";
|
||||
std::wstring text, result, expected;
|
||||
std::wstring text, result, expected, actual, verification = L"not-attempted";
|
||||
int hint = -1, to = command >= Direct ? command - Direct : -1;
|
||||
if (manual)
|
||||
{
|
||||
@@ -918,21 +937,34 @@ void App::transform(int command, int snippet)
|
||||
bool enabled;
|
||||
std::wstring &reason;
|
||||
std::wstring &text, &result, &expected;
|
||||
std::wstring &actual, &verification;
|
||||
int &hint, &to;
|
||||
int command;
|
||||
const std::vector<Layout> &layouts;
|
||||
~Audit()
|
||||
{
|
||||
if (enabled)
|
||||
diagnosticEvent(L"Transform outcome id=" + std::to_wstring(attempt) + L" reason=" + reason +
|
||||
L" foregroundSame=" + std::to_wstring(GetForegroundWindow() == target) +
|
||||
L" inputSame=" + std::to_wstring(app.inputVersion.load() == version) +
|
||||
L" elapsedMs=" + std::to_wstring(GetTickCount64() - start) +
|
||||
L" source=" + std::to_wstring(hint) + L" target=" + std::to_wstring(to) +
|
||||
L" expected=" + diagnosticText(expected) + L" copied=" + diagnosticText(text) +
|
||||
L" result=" + diagnosticText(result),
|
||||
reason != L"paste-sent-unverified" && reason != L"layout-only" &&
|
||||
reason != L"unchanged-expected");
|
||||
try
|
||||
{
|
||||
auto layout = [&](int index) { return index >= 0 && index < (int)layouts.size()
|
||||
? utf8(layouts[index].name + L" (" + layouts[index].id + L")") : std::string("Unknown"); };
|
||||
auto bounded = [](const std::wstring &value) { return utf8(value.substr(0, 4096)); };
|
||||
DiagnosticJson record{{"attempt", std::to_string(attempt)}, {"process", utf8(processName(target))},
|
||||
{"command", transformCommandName(command)}, {"source", layout(text.empty() ? hint : detect(text, layouts, hint))}, {"target", layout(to)},
|
||||
{"outcome", utf8(reason)}, {"verification", utf8(verification)},
|
||||
{"problem", reason != L"Paste verified" && reason != L"layout-only" &&
|
||||
!(command == 0 && reason == L"paste-sent-unverified")},
|
||||
{"elapsedMs", GetTickCount64() - start}, {"foregroundSame", GetForegroundWindow() == target},
|
||||
{"inputSame", app.inputVersion.load() == version}, {"original", bounded(text)},
|
||||
{"expected", bounded(result)}, {"actual", bounded(actual)},
|
||||
{"originalUnits", text.size()}, {"expectedUnits", result.size()}, {"actualUnits", actual.size()},
|
||||
{"textTruncated", text.size() > 4096 || result.size() > 4096 || actual.size() > 4096}};
|
||||
diagnosticOperation(record.dump(-1, ' ', true, DiagnosticJson::error_handler_t::replace));
|
||||
}
|
||||
catch (...) {} // Logging must never abort a completed input transaction.
|
||||
}
|
||||
} audit{*this, target, version, attempt, requestedAt, manual || snippet >= 0, reason, text, result, expected, hint, to};
|
||||
} audit{*this, target, version, attempt, requestedAt, manual || snippet >= 0, reason, text, result, expected,
|
||||
actual, verification, hint, to, command, available};
|
||||
try
|
||||
{
|
||||
auto valid = [&]
|
||||
@@ -1157,6 +1189,7 @@ void App::transform(int command, int snippet)
|
||||
}
|
||||
}
|
||||
bool sent = false;
|
||||
PasteProbe probe(config.writeLog && manual && result != text, text);
|
||||
diagnosticEvent(L"Conversion source=" + std::to_wstring(hint) + L" target=" + std::to_wstring(to) +
|
||||
L" changed=" + std::to_wstring(result != text));
|
||||
if (result != text)
|
||||
@@ -1167,10 +1200,27 @@ void App::transform(int command, int snippet)
|
||||
autoSelected = false;
|
||||
reason = result == text ? L"conversion-unchanged" :
|
||||
sent ? L"paste-sent-unverified" : L"paste-cancelled";
|
||||
if (result == text &&
|
||||
(std::none_of(text.begin(), text.end(), letter) ||
|
||||
(command >= Direct && detect(text, available, hint) == to)))
|
||||
reason = L"unchanged-expected";
|
||||
if (manual && result == text)
|
||||
{
|
||||
reason = L"Conversion unchanged";
|
||||
verification = L"calculated-result-equals-original";
|
||||
}
|
||||
if (sent && manual && config.writeLog)
|
||||
{
|
||||
verification = probe.status;
|
||||
if (tx.valid())
|
||||
{
|
||||
auto observed = probe.read((int)caretSteps(result));
|
||||
verification = probe.status;
|
||||
if (observed && tx.valid())
|
||||
{
|
||||
actual = *observed;
|
||||
reason = conversionReadbackOutcome(newlines(actual), newlines(text), newlines(result));
|
||||
}
|
||||
else reason = L"Paste not verified";
|
||||
}
|
||||
else verification = L"context-changed-after-paste";
|
||||
}
|
||||
if (sent && !tx.valid())
|
||||
reason = L"context-changed-after-paste";
|
||||
if (sent && command == Undo && previous && previous->autoCorrected)
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
#pragma once
|
||||
#include "../vendor/json.hpp"
|
||||
#include "platform.h"
|
||||
#include <sstream>
|
||||
|
||||
namespace ks
|
||||
{
|
||||
using DiagnosticJson = nlohmann::json;
|
||||
struct OperationLogRow
|
||||
{
|
||||
std::wstring time, process, command, source, target, outcome, details;
|
||||
bool problem = true;
|
||||
};
|
||||
inline std::vector<OperationLogRow> parseOperationLog(const std::wstring &contents)
|
||||
{
|
||||
std::vector<OperationLogRow> rows;
|
||||
std::wistringstream stream(contents);
|
||||
std::wstring line;
|
||||
OperationLogRow *legacy = nullptr;
|
||||
while (std::getline(stream, line))
|
||||
{
|
||||
if (!line.empty() && line.back() == L'\r')
|
||||
line.pop_back();
|
||||
if (line.empty())
|
||||
continue;
|
||||
auto marker = line.find(L" Operation {");
|
||||
if (marker != line.npos)
|
||||
{
|
||||
try
|
||||
{
|
||||
auto j = DiagnosticJson::parse(utf8(line.substr(marker + 11)));
|
||||
auto field = [&](const char *key) { return wide(j.value(key, std::string{})); };
|
||||
OperationLogRow row{line.substr(0, marker), field("process"), field("command"),
|
||||
field("source"), field("target"), field("outcome")};
|
||||
row.problem = j.value("problem", true);
|
||||
row.details =
|
||||
L"Operation: " + field("attempt") + L"\r\nResult: " + row.outcome +
|
||||
L"\r\nVerification: " + field("verification") + L"\r\nElapsed: " +
|
||||
std::to_wstring(j.value("elapsedMs", 0ULL)) + L" ms\r\n\r\nOriginal:\r\n" +
|
||||
field("original") + L"\r\n\r\nExpected result:\r\n" + field("expected") +
|
||||
L"\r\n\r\nRead back:\r\n" + field("actual") + L"\r\n\r\nForeground unchanged: " +
|
||||
(j.value("foregroundSame", false) ? L"yes" : L"no") + L"\r\nInput context unchanged: " +
|
||||
(j.value("inputSame", false) ? L"yes" : L"no") + L"\r\nText truncated: " +
|
||||
(j.value("textTruncated", false) ? L"yes (4096 UTF-16 units per field)" : L"no") +
|
||||
L"\r\n\r\nContext and stages:\r\n" + field("context");
|
||||
for (size_t at = 0; (at = row.details.find(L'\n', at)) != row.details.npos; ++at)
|
||||
if (at == 0 || row.details[at - 1] != L'\r')
|
||||
row.details.insert(at++, 1, L'\r');
|
||||
rows.push_back(std::move(row));
|
||||
legacy = nullptr;
|
||||
continue;
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
} // Preserve malformed and old records for diagnosis.
|
||||
}
|
||||
if (line.front() != L' ' || !legacy)
|
||||
{
|
||||
rows.push_back({L"", L"", L"Legacy / event", L"", L"", L"Unstructured event", line});
|
||||
legacy = &rows.back();
|
||||
}
|
||||
else
|
||||
legacy->details += L"\r\n" + line;
|
||||
}
|
||||
return rows;
|
||||
}
|
||||
inline std::wstring conversionReadbackOutcome(const std::wstring &actual, const std::wstring &original,
|
||||
const std::wstring &expected)
|
||||
{
|
||||
if (actual == expected)
|
||||
return L"Paste verified";
|
||||
if (actual == original)
|
||||
return L"Original text remained";
|
||||
return L"Readback differs";
|
||||
}
|
||||
} // namespace ks
|
||||
@@ -0,0 +1,162 @@
|
||||
#pragma once
|
||||
#include "platform.h"
|
||||
#include <uiautomation.h>
|
||||
|
||||
namespace ks
|
||||
{
|
||||
// Read-only UI Automation: never select, move the caret, or touch the clipboard.
|
||||
class PasteProbe
|
||||
{
|
||||
IUIAutomation2 *automation{};
|
||||
IUIAutomationElement *element{};
|
||||
IUIAutomationTextPattern *pattern{};
|
||||
|
||||
public:
|
||||
std::wstring status = L"not-requested";
|
||||
PasteProbe(bool enabled, const std::wstring &original)
|
||||
{
|
||||
if (!enabled)
|
||||
return;
|
||||
status = L"accessibility-unavailable";
|
||||
if (FAILED(CoCreateInstance(CLSID_CUIAutomation8, nullptr, CLSCTX_INPROC_SERVER, IID_IUIAutomation2,
|
||||
(void **)&automation)))
|
||||
return;
|
||||
automation->put_ConnectionTimeout(200);
|
||||
automation->put_TransactionTimeout(200);
|
||||
if (FAILED(automation->GetFocusedElement(&element)) || !element)
|
||||
return;
|
||||
BOOL password = TRUE;
|
||||
if (FAILED(element->get_CurrentIsPassword(&password)) || password)
|
||||
{
|
||||
status = L"protected-or-inaccessible-field";
|
||||
return;
|
||||
}
|
||||
if (FAILED(element->GetCurrentPatternAs(UIA_TextPatternId, IID_IUIAutomationTextPattern,
|
||||
(void **)&pattern)) ||
|
||||
!pattern)
|
||||
return;
|
||||
auto selected = read(1, false);
|
||||
auto normalize = [](std::wstring value)
|
||||
{
|
||||
for (size_t at = 0; (at = value.find(L"\r\n", at)) != value.npos;)
|
||||
value.erase(at, 1);
|
||||
return value;
|
||||
};
|
||||
if (!selected || normalize(*selected) != normalize(original))
|
||||
{
|
||||
if (selected)
|
||||
status = L"initial-selection-mismatch";
|
||||
if (pattern)
|
||||
pattern->Release();
|
||||
pattern = nullptr;
|
||||
return;
|
||||
}
|
||||
status = L"ready";
|
||||
}
|
||||
PasteProbe(const PasteProbe &) = delete;
|
||||
~PasteProbe()
|
||||
{
|
||||
if (pattern)
|
||||
pattern->Release();
|
||||
if (element)
|
||||
element->Release();
|
||||
if (automation)
|
||||
automation->Release();
|
||||
}
|
||||
std::optional<std::wstring> read(int characters, bool allowCaret = true)
|
||||
{
|
||||
if (!pattern)
|
||||
return {};
|
||||
if (characters <= 0 || characters > 4096)
|
||||
{
|
||||
status = L"readback-size-limit";
|
||||
return {};
|
||||
}
|
||||
IUIAutomationElement *focused{};
|
||||
BOOL same = FALSE;
|
||||
if (SUCCEEDED(automation->GetFocusedElement(&focused)) && focused)
|
||||
{
|
||||
automation->CompareElements(element, focused, &same);
|
||||
}
|
||||
if (!same)
|
||||
{
|
||||
if (focused)
|
||||
focused->Release();
|
||||
status = L"focused-control-changed";
|
||||
return {};
|
||||
}
|
||||
BOOL password = TRUE;
|
||||
if (FAILED(focused->get_CurrentIsPassword(&password)) || password)
|
||||
{
|
||||
focused->Release();
|
||||
status = L"protected-or-inaccessible-field";
|
||||
return {};
|
||||
}
|
||||
// Native Edit accessibility proxies can retain a text snapshot in the
|
||||
// pattern object. Obtain a fresh pattern after paste, not the pre-paste one.
|
||||
pattern->Release();
|
||||
pattern = nullptr;
|
||||
HRESULT refreshed =
|
||||
focused->GetCurrentPatternAs(UIA_TextPatternId, IID_IUIAutomationTextPattern, (void **)&pattern);
|
||||
focused->Release();
|
||||
if (FAILED(refreshed) || !pattern)
|
||||
{
|
||||
status = L"accessibility-unavailable";
|
||||
return {};
|
||||
}
|
||||
IUIAutomationTextRangeArray *ranges{};
|
||||
if (FAILED(pattern->GetSelection(&ranges)) || !ranges)
|
||||
{
|
||||
status = L"selection-unavailable";
|
||||
return {};
|
||||
}
|
||||
int count{};
|
||||
ranges->get_Length(&count);
|
||||
IUIAutomationTextRange *range{};
|
||||
if (count == 1)
|
||||
ranges->GetElement(0, &range);
|
||||
ranges->Release();
|
||||
if (!range)
|
||||
{
|
||||
status = L"selection-ambiguous";
|
||||
return {};
|
||||
}
|
||||
int difference{};
|
||||
HRESULT hr = range->CompareEndpoints(TextPatternRangeEndpoint_Start, range,
|
||||
TextPatternRangeEndpoint_End, &difference);
|
||||
if (SUCCEEDED(hr) && difference == 0)
|
||||
{
|
||||
if (!allowCaret)
|
||||
{
|
||||
range->Release();
|
||||
status = L"initial-selection-unavailable";
|
||||
return {};
|
||||
}
|
||||
int moved{};
|
||||
hr = range->MoveEndpointByUnit(TextPatternRangeEndpoint_Start, TextUnit_Character, -characters,
|
||||
&moved);
|
||||
if (SUCCEEDED(hr) && moved != -characters)
|
||||
hr = E_FAIL;
|
||||
}
|
||||
BSTR value{};
|
||||
if (SUCCEEDED(hr))
|
||||
hr = range->GetText(8193, &value);
|
||||
range->Release();
|
||||
if (FAILED(hr))
|
||||
{
|
||||
SysFreeString(value);
|
||||
status = L"range-read-failed";
|
||||
return {};
|
||||
}
|
||||
std::wstring text(value ? value : L"", SysStringLen(value));
|
||||
SysFreeString(value);
|
||||
if (text.size() > 8192)
|
||||
{
|
||||
status = L"readback-size-limit";
|
||||
return {};
|
||||
}
|
||||
status = L"read-back-via-accessibility";
|
||||
return text;
|
||||
}
|
||||
};
|
||||
} // namespace ks
|
||||
+21
-2
@@ -1,6 +1,8 @@
|
||||
#include "platform.h"
|
||||
#include "../vendor/json.hpp"
|
||||
#include <algorithm>
|
||||
#include <fstream>
|
||||
#include <iomanip>
|
||||
#include <imm.h>
|
||||
#include <map>
|
||||
#include <mutex>
|
||||
@@ -624,6 +626,22 @@ void diagnosticEvent(const std::wstring &message, bool problem)
|
||||
if (!incident.empty())
|
||||
log(incident);
|
||||
}
|
||||
void diagnosticOperation(const std::string &json)
|
||||
{
|
||||
std::lock_guard lock(logGate);
|
||||
if (!loggingEnabled) return;
|
||||
auto record = nlohmann::json::parse(json);
|
||||
if (!record.value("problem", true))
|
||||
{
|
||||
// Keep successful operations only in the bounded, short-lived context.
|
||||
// They are persisted with a subsequent failure, never as standalone rows.
|
||||
diagnosticBuffer.record(L"Completed operation " + wide(record.dump(-1, ' ', true,
|
||||
nlohmann::json::error_handler_t::replace)), false, GetTickCount64());
|
||||
return;
|
||||
}
|
||||
record["context"] = utf8(diagnosticBuffer.record(L"Operation complete", true, GetTickCount64()));
|
||||
log(L"Operation " + wide(record.dump(-1, ' ', true, nlohmann::json::error_handler_t::replace)));
|
||||
}
|
||||
void log(const std::wstring &message)
|
||||
{
|
||||
std::lock_guard lock(logGate);
|
||||
@@ -645,8 +663,9 @@ void log(const std::wstring &message)
|
||||
std::ofstream f(path, std::ios::app);
|
||||
SYSTEMTIME t;
|
||||
GetLocalTime(&t);
|
||||
f << t.wYear << '-' << t.wMonth << '-' << t.wDay << ' ' << t.wHour << ':' << t.wMinute << ':'
|
||||
<< t.wSecond << ' ' << utf8(message) << '\n';
|
||||
f << std::setfill('0') << std::setw(4) << t.wYear << '-' << std::setw(2) << t.wMonth << '-'
|
||||
<< std::setw(2) << t.wDay << ' ' << std::setw(2) << t.wHour << ':' << std::setw(2) << t.wMinute << ':'
|
||||
<< std::setw(2) << t.wSecond << ' ' << utf8(message) << '\n';
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
|
||||
@@ -66,6 +66,7 @@ class DiagnosticBuffer
|
||||
};
|
||||
void setLoggingEnabled(bool enabled);
|
||||
void diagnosticEvent(const std::wstring &message, bool problem = false);
|
||||
void diagnosticOperation(const std::string &json);
|
||||
std::wstring readDiagnosticLog(bool previous);
|
||||
void clearDiagnosticLog(bool previous);
|
||||
} // namespace ks
|
||||
|
||||
+9
-244
@@ -4,6 +4,7 @@
|
||||
#include "ocr.h"
|
||||
#include "richtext.h"
|
||||
#include "ui_geometry.h"
|
||||
#include "diagnostics.h"
|
||||
#include <algorithm>
|
||||
#include <commctrl.h>
|
||||
#include <commdlg.h>
|
||||
@@ -34,7 +35,7 @@ struct WindowLayout
|
||||
bool scrolling{}, arranging{}, history{};
|
||||
int wheelRemainder{}, horizontalWheelRemainder{};
|
||||
std::vector<std::pair<HWND, RECT>> controls; // Original 96-DPI geometry; never rescale rounded pixels.
|
||||
HWND stretch{}, bottomLeft{}, bottomRight{};
|
||||
HWND stretch{}, bottomLeft{}, bottomRight{}, details{};
|
||||
bool autoOptions{};
|
||||
};
|
||||
WindowLayout *windowLayoutState(HWND w)
|
||||
@@ -88,6 +89,11 @@ void arrangeWindow(HWND w)
|
||||
next.right += extraWidth;
|
||||
next.bottom += extraHeight;
|
||||
}
|
||||
if (child == layout->details)
|
||||
{
|
||||
next.right += extraWidth;
|
||||
OffsetRect(&next, 0, extraHeight);
|
||||
}
|
||||
if (child == layout->bottomLeft || child == layout->bottomRight)
|
||||
OffsetRect(&next, child == layout->bottomRight ? extraWidth : 0, extraHeight);
|
||||
if (layout->autoOptions)
|
||||
@@ -311,7 +317,7 @@ std::wstring value(HWND w, int id)
|
||||
text.resize(n);
|
||||
return text;
|
||||
}
|
||||
void value(HWND w, int id, const std::wstring &text)
|
||||
[[maybe_unused]] void value(HWND w, int id, const std::wstring &text)
|
||||
{
|
||||
SetDlgItemTextW(w, id, text.c_str());
|
||||
}
|
||||
@@ -1573,248 +1579,7 @@ void showHistory(App &app, bool visible)
|
||||
}
|
||||
#endif
|
||||
#ifndef KS_RESIDENT_APP
|
||||
namespace
|
||||
{
|
||||
constexpr int LogChoice = 601, LogRows = 602, LogRefresh = 603, LogStatus = 604, LogCopy = 605,
|
||||
LogClear = 606;
|
||||
void fitLogDetails(HWND list)
|
||||
{
|
||||
if (GetPropW(list, L"FittingLogDetails") || Header_GetItemCount(ListView_GetHeader(list)) != 4) return;
|
||||
SetPropW(list, L"FittingLogDetails", (HANDLE)1);
|
||||
RECT client{};
|
||||
GetClientRect(list, &client);
|
||||
int remaining = client.right;
|
||||
for (int column = 0; column < 3; ++column)
|
||||
remaining -= ListView_GetColumnWidth(list, column);
|
||||
int width = std::max(scale(list, 120), remaining);
|
||||
if (ListView_GetColumnWidth(list, 3) != width)
|
||||
ListView_SetColumnWidth(list, 3, width);
|
||||
RemovePropW(list, L"FittingLogDetails");
|
||||
}
|
||||
LRESULT CALLBACK logListResizeProc(HWND w, UINT msg, WPARAM wp, LPARAM lp, UINT_PTR, DWORD_PTR)
|
||||
{
|
||||
auto result = DefSubclassProc(w, msg, wp, lp);
|
||||
if (msg == WM_SIZE)
|
||||
fitLogDetails(w);
|
||||
if (msg == WM_NOTIFY && lp)
|
||||
{
|
||||
auto *header = reinterpret_cast<NMHEADERW *>(lp);
|
||||
if (header->hdr.hwndFrom == ListView_GetHeader(w) &&
|
||||
(header->hdr.code == HDN_ITEMCHANGEDW || header->hdr.code == HDN_ITEMCHANGEDA) &&
|
||||
header->iItem >= 0 && header->iItem < 3)
|
||||
fitLogDetails(w);
|
||||
}
|
||||
if (msg == WM_NCDESTROY) RemoveWindowSubclass(w, logListResizeProc, 3);
|
||||
return result;
|
||||
}
|
||||
void copyLogRows(HWND w, App &app)
|
||||
{
|
||||
HWND list = GetDlgItem(w, LogRows);
|
||||
std::wstring copied;
|
||||
for (int row = ListView_GetNextItem(list, -1, LVNI_SELECTED); row >= 0;
|
||||
row = ListView_GetNextItem(list, row, LVNI_SELECTED))
|
||||
{
|
||||
for (int column = 0; column < 4; ++column)
|
||||
{
|
||||
std::vector<wchar_t> text(32768);
|
||||
ListView_GetItemText(list, row, column, text.data(), (int)text.size());
|
||||
if (column)
|
||||
copied += L'\t';
|
||||
copied += text.data();
|
||||
}
|
||||
copied += L"\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);
|
||||
}
|
||||
}
|
||||
void refreshLogView(HWND w, App &app)
|
||||
{
|
||||
const bool previous = SendDlgItemMessageW(w, LogChoice, CB_GETCURSEL, 0, 0) == 1;
|
||||
std::wstring contents;
|
||||
try
|
||||
{
|
||||
contents = readDiagnosticLog(previous);
|
||||
}
|
||||
catch (const std::exception &e)
|
||||
{
|
||||
contents = L"Could not read the log: " + wide(e.what());
|
||||
}
|
||||
HWND list = GetDlgItem(w, LogRows);
|
||||
SendMessageW(list, WM_SETREDRAW, FALSE, 0);
|
||||
ListView_DeleteAllItems(list);
|
||||
std::wistringstream lines(contents);
|
||||
std::wstring line, timestamp;
|
||||
int row = 0;
|
||||
while (std::getline(lines, line))
|
||||
{
|
||||
if (!line.empty() && line.back() == L'\r')
|
||||
line.pop_back();
|
||||
if (line.empty())
|
||||
continue;
|
||||
auto firstSpace = line.find(L' ');
|
||||
auto secondSpace = firstSpace == line.npos ? line.npos : line.find(L' ', firstSpace + 1);
|
||||
if (iswdigit(line.front()) && line.find(L'-') < firstSpace && secondSpace != line.npos)
|
||||
{
|
||||
timestamp = line.substr(0, secondSpace);
|
||||
line.erase(0, secondSpace + 1);
|
||||
}
|
||||
auto id = line.find(L" id=");
|
||||
std::wstring attempt;
|
||||
if (id != line.npos)
|
||||
{
|
||||
auto end = line.find(L' ', id + 4);
|
||||
attempt = line.substr(id + 4, end == line.npos ? end : end - id - 4);
|
||||
}
|
||||
std::wstring event = line;
|
||||
if (event.starts_with(L" ageMs="))
|
||||
{
|
||||
auto end = event.find(L' ', 8);
|
||||
if (end != event.npos)
|
||||
event.erase(0, end + 1);
|
||||
}
|
||||
auto equals = event.find(L'=');
|
||||
if (equals != event.npos)
|
||||
{
|
||||
auto key = event.rfind(L' ', equals);
|
||||
if (key != event.npos)
|
||||
event.resize(key);
|
||||
}
|
||||
LVITEMW item{};
|
||||
item.mask = LVIF_TEXT;
|
||||
item.iItem = row;
|
||||
item.pszText = timestamp.data();
|
||||
ListView_InsertItem(list, &item);
|
||||
ListView_SetItemText(list, row, 1, attempt.data());
|
||||
ListView_SetItemText(list, row, 2, event.data());
|
||||
ListView_SetItemText(list, row, 3, line.data());
|
||||
++row;
|
||||
}
|
||||
SendMessageW(list, WM_SETREDRAW, TRUE, 0);
|
||||
InvalidateRect(list, nullptr, TRUE);
|
||||
if (row)
|
||||
ListView_EnsureVisible(list, row - 1, FALSE);
|
||||
fitLogDetails(list);
|
||||
value(w, LogStatus, app.settings.writeLog ? L"Logging enabled: failed operations only."
|
||||
: L"Logging disabled. Showing previously saved entries.");
|
||||
}
|
||||
LRESULT CALLBACK logProc(HWND w, UINT msg, WPARAM wp, LPARAM lp)
|
||||
{
|
||||
auto *app = reinterpret_cast<App *>(GetWindowLongPtrW(w, GWLP_USERDATA));
|
||||
if (msg == WM_NCCREATE)
|
||||
{
|
||||
SetWindowLongPtrW(w, GWLP_USERDATA, (LONG_PTR)((CREATESTRUCTW *)lp)->lpCreateParams);
|
||||
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) == 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 && ((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:
|
||||
app->logWindow = nullptr;
|
||||
disposeFont(w);
|
||||
break;
|
||||
}
|
||||
return DefWindowProcW(w, msg, wp, lp);
|
||||
}
|
||||
}
|
||||
void showLogs(App &app)
|
||||
{
|
||||
if (app.logWindow)
|
||||
{
|
||||
showRequestedWindow(app.logWindow);
|
||||
activateWindow(app.logWindow);
|
||||
return;
|
||||
}
|
||||
auto w = makeWindow(app, L"KSwitcher.Logs", L"KSwitcher log", logProc, 900, 590);
|
||||
app.logWindow = w;
|
||||
auto choice = control(w, L"COMBOBOX", L"", WS_TABSTOP | CBS_DROPDOWNLIST, LogChoice, 16, 16, 300, 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, 330, 14, 110);
|
||||
button(w, L"Copy selected", LogCopy, 454, 14, 140);
|
||||
button(w, L"Clear log", LogClear, 608, 14, 110);
|
||||
control(w, L"STATIC", L"", 0, LogStatus, 16, 52, 860, 22);
|
||||
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, 82, 868, 454);
|
||||
ListView_SetExtendedListViewStyle(list, LVS_EX_FULLROWSELECT | LVS_EX_GRIDLINES | LVS_EX_DOUBLEBUFFER);
|
||||
SetWindowSubclass(list, logListResizeProc, 3, 0);
|
||||
const wchar_t *titles[] = {L"Time", L"Attempt", L"Event", L"Details"};
|
||||
const int widths[] = {155, 65, 195, 900};
|
||||
for (int i = 0; i < 4; ++i)
|
||||
{
|
||||
LVCOLUMNW column{};
|
||||
column.mask = LVCF_TEXT | LVCF_WIDTH;
|
||||
column.pszText = const_cast<wchar_t *>(titles[i]);
|
||||
column.cx = scale(w, widths[i]);
|
||||
ListView_InsertColumn(list, i, &column);
|
||||
}
|
||||
auto hint = control(w, L"STATIC", L"Select rows to copy. Ctrl+A selects all; Ctrl+C copies selected rows.",
|
||||
0, 0, 16, 551, 720, 22);
|
||||
button(w, L"Close", IDCANCEL, 774, 548, 110);
|
||||
auto *layout = windowLayoutState(w);
|
||||
layout->stretch = list;
|
||||
layout->bottomLeft = hint;
|
||||
layout->bottomRight = GetDlgItem(w, IDCANCEL);
|
||||
arrangeWindow(w);
|
||||
refreshLogView(w, app);
|
||||
showRequestedWindow(w);
|
||||
activateWindow(w);
|
||||
}
|
||||
#include "ui_log.inl"
|
||||
#else
|
||||
void showLogs(App &app) { launchConfiguration(app, false, true); }
|
||||
#endif
|
||||
|
||||
+357
@@ -0,0 +1,357 @@
|
||||
// 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<OperationLogRow> rows;
|
||||
std::vector<size_t> visible;
|
||||
int sortColumn = 0;
|
||||
bool descending = true, rendering = false;
|
||||
};
|
||||
LogViewState *logState(HWND w)
|
||||
{
|
||||
return reinterpret_cast<LogViewState *>(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<wchar_t *>(row.time.c_str());
|
||||
ListView_InsertItem(list, &item);
|
||||
for (int column = 1; column < 6; ++column)
|
||||
ListView_SetItemText(list, (int)i, column, const_cast<wchar_t *>(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<std::wstring> &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<std::wstring> 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<App *>(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"Command", 206, 78, 200);
|
||||
control(w, L"COMBOBOX", L"", WS_TABSTOP | CBS_DROPDOWNLIST | WS_VSCROLL, LogCommand, 206, 101, 204, 300);
|
||||
label(w, L"Result", 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<wchar_t *>(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"Close", 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);
|
||||
}
|
||||
+1
-1
@@ -2,7 +2,7 @@
|
||||
#include "platform.h"
|
||||
namespace ks
|
||||
{
|
||||
inline constexpr wchar_t AppVersion[] = L"0.6.17";
|
||||
inline constexpr wchar_t AppVersion[] = L"0.6.18";
|
||||
inline constexpr UINT UpdateMessage = WM_APP + 7;
|
||||
inline constexpr UINT UpdateCheckIntervalMs = 6 * 60 * 60 * 1000;
|
||||
inline constexpr UINT UpdateNotificationRetryMs = 5 * 60 * 1000;
|
||||
|
||||
Reference in New Issue
Block a user