Release v0.6.39: isolate history UI and image clipboard memory
This commit is contained in:
+86
-14
@@ -15,6 +15,7 @@ namespace ks
|
||||
{
|
||||
#include "app_sync.inl"
|
||||
App *App::current = nullptr;
|
||||
#include "app_history.inl"
|
||||
namespace
|
||||
{
|
||||
const char *transformCommandName(int command)
|
||||
@@ -71,7 +72,7 @@ class Transaction
|
||||
App &app;
|
||||
HWND target;
|
||||
uint64_t version;
|
||||
Clip snapshot;
|
||||
ClipboardSnapshot snapshot;
|
||||
DWORD expected{};
|
||||
bool captured{};
|
||||
mutable bool invalidLogged{};
|
||||
@@ -87,7 +88,7 @@ class Transaction
|
||||
try
|
||||
{
|
||||
expected = GetClipboardSequenceNumber();
|
||||
snapshot = readClipboard(app.window, 64 * 1024 * 1024);
|
||||
snapshot.capture(app.window, expected);
|
||||
if (expected != GetClipboardSequenceNumber())
|
||||
throw std::runtime_error("Clipboard changed while taking snapshot");
|
||||
captured = true;
|
||||
@@ -103,7 +104,7 @@ class Transaction
|
||||
if (captured && GetClipboardSequenceNumber() == expected)
|
||||
try
|
||||
{
|
||||
app.ownSequence = writeClipboard(app.window, snapshot);
|
||||
app.ownSequence = snapshot.restore(app.window, expected);
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
@@ -131,7 +132,7 @@ class Transaction
|
||||
if (GetClipboardSequenceNumber() != expected)
|
||||
{
|
||||
auto now = GetClipboardSequenceNumber();
|
||||
if (adopted.empty() || !ownsClipboard(target) || readClipboard(app.window).text() != adopted ||
|
||||
if (adopted.empty() || !ownsClipboard(target) || readClipboard(app.window, 32 * 1024 * 1024, ClipboardRead::Selection).text() != adopted ||
|
||||
GetClipboardSequenceNumber() != now)
|
||||
throw std::runtime_error("Operation cancelled: another application changed the clipboard");
|
||||
expected = now;
|
||||
@@ -153,7 +154,7 @@ class Transaction
|
||||
if (GetClipboardOwner() != app.window)
|
||||
throw std::runtime_error("Clipboard changed immediately after writing");
|
||||
auto text = c.text();
|
||||
if ((!text.empty() && readClipboard(app.window).text() != text) ||
|
||||
if ((!text.empty() && readClipboard(app.window, 32 * 1024 * 1024, ClipboardRead::Selection).text() != text) ||
|
||||
GetClipboardSequenceNumber() != expected)
|
||||
throw std::runtime_error("Clipboard changed while verifying our write");
|
||||
}
|
||||
@@ -227,7 +228,7 @@ class Transaction
|
||||
changed != GetClipboardSequenceNumber())
|
||||
throw std::runtime_error("Operation cancelled: copy did not belong to the target application");
|
||||
expected = changed;
|
||||
auto clip = readClipboard(app.window);
|
||||
auto clip = readClipboard(app.window, 32 * 1024 * 1024, ClipboardRead::Selection);
|
||||
auto text = clip.text();
|
||||
diagnosticEvent(L"Copy result number=" + std::to_wstring(number) + L" attempts=" +
|
||||
std::to_wstring(attempts) + L" elapsedMs=" + std::to_wstring(GetTickCount64() - started) +
|
||||
@@ -405,7 +406,8 @@ class Transaction
|
||||
}
|
||||
};
|
||||
} // namespace
|
||||
App::App(HINSTANCE h, bool test, const std::filesystem::path &dataRoot) : instance(h), smoke(test)
|
||||
App::App(HINSTANCE h, bool test, const std::filesystem::path &dataRoot, HWND service)
|
||||
: historyService(service), instance(h), smoke(test)
|
||||
{
|
||||
updateNotifier = notifyUpdateAvailable;
|
||||
current = this;
|
||||
@@ -422,7 +424,7 @@ App::App(HINSTANCE h, bool test, const std::filesystem::path &dataRoot) : instan
|
||||
registrationWarning = L"Settings were not loaded: " + wide(e.what());
|
||||
}
|
||||
setLoggingEnabled(settings.writeLog);
|
||||
if (settings.autoCorrect)
|
||||
if (settings.autoCorrect && !historyService)
|
||||
try { autoPredictor.configure(std::filesystem::path(executable()).parent_path() / L"dictionaries", settings.autoLanguages); }
|
||||
catch (const std::exception &e)
|
||||
{
|
||||
@@ -441,6 +443,8 @@ App::App(HINSTANCE h, bool test, const std::filesystem::path &dataRoot) : instan
|
||||
}
|
||||
App::~App()
|
||||
{
|
||||
if (auto host = historyController(historyProcess)) PostMessageW(host, WM_CLOSE, 0, 0);
|
||||
if (historyProcess) CloseHandle(historyProcess);
|
||||
if (notificationRegistration) CloseHandle(notificationRegistration);
|
||||
if (syncWorker.joinable()) syncWorker.join();
|
||||
if (updateWorker.joinable())
|
||||
@@ -688,6 +692,7 @@ void App::reloadLayouts()
|
||||
}
|
||||
void App::clearHistory()
|
||||
{
|
||||
if (historyService) { remoteHistory("clear"); history->reload(); return; }
|
||||
if (busy) throw std::runtime_error("Wait for the current operation to finish.");
|
||||
if (history) history->clear();
|
||||
if (historyWindow) refreshHistory(*this);
|
||||
@@ -1523,6 +1528,7 @@ void App::transform(int command, int snippet)
|
||||
}
|
||||
void App::pasteEntry(std::wstring id, bool plain)
|
||||
{
|
||||
if (historyService) { remoteHistory("paste", id, plain); return; }
|
||||
if (installingUpdate) return;
|
||||
if (!history || busy.exchange(true))
|
||||
return;
|
||||
@@ -1544,10 +1550,20 @@ void App::pasteEntry(std::wstring id, bool plain)
|
||||
} reset{busy};
|
||||
try
|
||||
{
|
||||
for (int i = 0; i < 20 && !modifiersReleased(); ++i)
|
||||
// Foreground activation crosses a process boundary when the
|
||||
// history UI closes. Let the destination finish activation,
|
||||
// while retaining the same input-version cancellation guard.
|
||||
for (int i = 0; i < 20 && inputVersion == version &&
|
||||
(!modifiersReleased() || GetForegroundWindow() != target); ++i)
|
||||
Sleep(10);
|
||||
if (!modifiersReleased() || GetForegroundWindow() != target || inputVersion != version)
|
||||
{
|
||||
log(L"History paste cancelled before transaction: foreground=" +
|
||||
std::to_wstring(GetForegroundWindow() == target) + L" modifiers=" +
|
||||
std::to_wstring(modifiersReleased()) + L" expectedVersion=" + std::to_wstring(version) +
|
||||
L" actualVersion=" + std::to_wstring(inputVersion.load()));
|
||||
return;
|
||||
}
|
||||
auto clip = history->load(id);
|
||||
if (plain)
|
||||
{
|
||||
@@ -1557,7 +1573,7 @@ void App::pasteEntry(std::wstring id, bool plain)
|
||||
clip = textClip(text);
|
||||
}
|
||||
Transaction tx(*this, target, version);
|
||||
tx.paste(clip);
|
||||
log(tx.paste(clip) ? L"History paste: simulated paste completed" : L"History paste cancelled before simulated paste");
|
||||
}
|
||||
catch (const std::exception &e)
|
||||
{
|
||||
@@ -1599,7 +1615,8 @@ void App::captureHistory()
|
||||
}
|
||||
return;
|
||||
}
|
||||
auto clip = readClipboard(window, (size_t)config.maxImageMiB * 1024 * 1024);
|
||||
auto clip = readClipboard(window, (size_t)config.maxImageMiB * 1024 * 1024,
|
||||
ClipboardRead::WithoutImages, sequence);
|
||||
if (sequence != GetClipboardSequenceNumber())
|
||||
return;
|
||||
for (auto &f : clip.formats)
|
||||
@@ -1618,6 +1635,7 @@ void App::captureHistory()
|
||||
}
|
||||
void App::recognizeEntry(std::wstring id)
|
||||
{
|
||||
if (historyService) { remoteHistory("ocr", id); return; }
|
||||
if (!history || busy.exchange(true))
|
||||
return;
|
||||
auto sequence = GetClipboardSequenceNumber();
|
||||
@@ -1788,7 +1806,37 @@ LRESULT CALLBACK App::windowProc(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp)
|
||||
auto *app = current;
|
||||
if (!app)
|
||||
return DefWindowProcW(hwnd, msg, wp, lp);
|
||||
if (msg == app->taskbarCreated && app->taskbarCreated)
|
||||
if (app->historyService) {
|
||||
if (msg == HistoryHostShow || msg == HistoryHostRefresh) {
|
||||
try {
|
||||
app->history->reload();
|
||||
if (msg == HistoryHostShow) showHistory(*app);
|
||||
else if (IsWindowVisible(app->historyWindow)) refreshHistory(*app);
|
||||
} catch (const std::exception &e) { app->report(wide(e.what())); }
|
||||
return 0;
|
||||
}
|
||||
if (msg == WM_TIMER) {
|
||||
if (!IsWindow(app->historyService)) PostMessageW(hwnd, WM_CLOSE, 0, 0);
|
||||
return 0;
|
||||
}
|
||||
if (msg == WM_CLOSE) {
|
||||
EnumThreadWindows(GetCurrentThreadId(), [](HWND child, LPARAM) -> BOOL {
|
||||
wchar_t name[64]{}; GetClassNameW(child, name, 64);
|
||||
if (std::wstring(name) == L"KSwitcher.Entry" || std::wstring(name) == L"KSwitcher.History")
|
||||
DestroyWindow(child);
|
||||
return TRUE;
|
||||
}, 0);
|
||||
DestroyWindow(hwnd);
|
||||
return 0;
|
||||
}
|
||||
if (msg == WM_DESTROY) { PostQuitMessage(0); return 0; }
|
||||
if (msg == ErrorMessage) {
|
||||
std::unique_ptr<std::wstring> text(reinterpret_cast<std::wstring *>(lp));
|
||||
MessageBoxW(app->historyWindow, text->c_str(), L"Clipboard history", MB_OK | MB_ICONWARNING);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
if (!app->historyService && msg == app->taskbarCreated && app->taskbarCreated)
|
||||
{
|
||||
app->addTray();
|
||||
return 0;
|
||||
@@ -1902,6 +1950,7 @@ LRESULT CALLBACK App::windowProc(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp)
|
||||
case WM_COPYDATA:
|
||||
{
|
||||
auto *data = (COPYDATASTRUCT *)lp;
|
||||
if (data && data->dwData == HistoryRequest) return app->historyRequest((HWND)wp, *data);
|
||||
if (data && data->dwData >= ConfigRead && data->dwData < ConfigEnd)
|
||||
{
|
||||
if (app->installingUpdate) return FALSE;
|
||||
@@ -1988,7 +2037,7 @@ int App::execute()
|
||||
WNDCLASSW cls{};
|
||||
cls.hInstance = instance;
|
||||
cls.lpfnWndProc = windowProc;
|
||||
cls.lpszClassName = L"KSwitcher.Main";
|
||||
cls.lpszClassName = historyService ? HistoryHostClass : L"KSwitcher.Main";
|
||||
cls.hIcon = LoadIconW(instance, MAKEINTRESOURCEW(IDI_KSWITCHER));
|
||||
RegisterClassW(&cls);
|
||||
window = CreateWindowExW(WS_EX_TOOLWINDOW, cls.lpszClassName, L"KSwitcher", WS_POPUP, 0, 0, 0, 0, nullptr,
|
||||
@@ -2022,6 +2071,29 @@ int App::execute()
|
||||
}
|
||||
CoUninitialize();
|
||||
});
|
||||
if (historyService) {
|
||||
try {
|
||||
auto bytes = unprotect(readFile(data / L"history-view.bin", 4 * 1024 * 1024));
|
||||
auto state = HistoryJson::parse(bytes);
|
||||
historySelection = wide(state.value("selection", std::string{}));
|
||||
historyTop = wide(state.value("top", std::string{}));
|
||||
historyQuery = wide(state.value("query", std::string{}));
|
||||
} catch (...) {}
|
||||
showHistory(*this);
|
||||
remoteHistory("ready", std::to_wstring(reinterpret_cast<uintptr_t>(historyWindow)));
|
||||
SetTimer(window, 1, 1000, nullptr);
|
||||
MSG message{};
|
||||
while (GetMessageW(&message, nullptr, 0, 0) > 0) {
|
||||
if (message.message == WM_KEYDOWN && message.wParam == VK_ESCAPE && historyWindow &&
|
||||
(message.hwnd == historyWindow || IsChild(historyWindow, message.hwnd))) {
|
||||
SendMessageW(historyWindow, WM_CLOSE, 0, 0);
|
||||
continue;
|
||||
}
|
||||
if (historyWindow && message.message == WM_SYSCHAR && IsDialogMessageW(historyWindow, &message)) continue;
|
||||
TranslateMessage(&message); DispatchMessageW(&message);
|
||||
}
|
||||
return int(message.wParam);
|
||||
}
|
||||
keyboardHook = SetWindowsHookExW(WH_KEYBOARD_LL, keyProc, instance, 0);
|
||||
mouseHook = SetWindowsHookExW(WH_MOUSE_LL, mouseProc, instance, 0);
|
||||
registerHotkeys(settings);
|
||||
@@ -2058,7 +2130,7 @@ int App::execute()
|
||||
continue;
|
||||
}
|
||||
// Preserve history-specific Enter/Shift+Enter and Ctrl+digit handling.
|
||||
if (historyWindow && msg.message == WM_SYSCHAR && IsDialogMessageW(historyWindow, &msg))
|
||||
if (!isolateHistory && historyWindow && msg.message == WM_SYSCHAR && IsDialogMessageW(historyWindow, &msg))
|
||||
continue;
|
||||
if (settingsWindow && IsDialogMessageW(settingsWindow, &msg))
|
||||
continue;
|
||||
|
||||
@@ -33,6 +33,10 @@ class App
|
||||
{
|
||||
public:
|
||||
HWND window{}, historyWindow{}, settingsWindow{}, snippetsWindow{};
|
||||
HWND historyService{}; // Non-null only in the short-lived history UI process.
|
||||
bool isolateHistory{};
|
||||
unsigned historyEntryWindows{};
|
||||
std::wstring historySelection, historyTop, historyQuery;
|
||||
HINSTANCE instance{};
|
||||
std::filesystem::path data;
|
||||
std::vector<Layout> layouts;
|
||||
@@ -55,7 +59,7 @@ class App
|
||||
bool smoke{};
|
||||
int startCommand{};
|
||||
std::wstring updateStartupEvent;
|
||||
App(HINSTANCE, bool = false, const std::filesystem::path &dataRoot = {});
|
||||
App(HINSTANCE, bool = false, const std::filesystem::path &dataRoot = {}, HWND historyService = nullptr);
|
||||
~App();
|
||||
int execute();
|
||||
void dispatch(int);
|
||||
@@ -72,6 +76,11 @@ class App
|
||||
void applySettings(const Settings &);
|
||||
void reloadLayouts();
|
||||
void clearHistory();
|
||||
void openHistoryProcess();
|
||||
void refreshHistoryProcess();
|
||||
void remoteHistory(const char *action, const std::wstring &id = {}, bool plain = false);
|
||||
LRESULT historyRequest(HWND, const COPYDATASTRUCT &);
|
||||
void saveHistoryView(const std::wstring &, const std::wstring &, const std::wstring &);
|
||||
void clearLog(bool previous);
|
||||
void setHotkeyCapture(HWND);
|
||||
bool isHotkey(DWORD, UINT);
|
||||
@@ -88,6 +97,7 @@ class App
|
||||
friend struct InputRegressionAccess;
|
||||
void releaseIdleMemory(ULONGLONG now);
|
||||
HHOOK keyboardHook{}, mouseHook{};
|
||||
HANDLE historyProcess{};
|
||||
std::mutex jobsMutex;
|
||||
std::condition_variable jobsReady;
|
||||
std::deque<std::function<void()>> jobs;
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
// History UI has no clipboard listener or hooks. The resident remains the only writer.
|
||||
namespace {
|
||||
constexpr ULONG_PTR HistoryRequest = 0x4b530200;
|
||||
constexpr UINT HistoryHostShow = WM_APP + 71, HistoryHostRefresh = WM_APP + 72;
|
||||
constexpr wchar_t HistoryHostClass[] = L"KSwitcher.HistoryHost";
|
||||
using HistoryJson = nlohmann::json;
|
||||
HWND historyController(HANDLE process)
|
||||
{
|
||||
if (!process || WaitForSingleObject(process, 0) == WAIT_OBJECT_0) return nullptr;
|
||||
auto window = FindWindowW(HistoryHostClass, nullptr);
|
||||
DWORD pid{}; GetWindowThreadProcessId(window, &pid);
|
||||
return pid == GetProcessId(process) ? window : nullptr;
|
||||
}
|
||||
}
|
||||
void App::openHistoryProcess()
|
||||
{
|
||||
auto foreground = GetForegroundWindow();
|
||||
if (foreground != window && foreground != historyWindow) pasteTarget = foreground;
|
||||
if (historyProcess && WaitForSingleObject(historyProcess, 0) == WAIT_OBJECT_0) {
|
||||
CloseHandle(historyProcess); historyProcess = nullptr; historyWindow = nullptr;
|
||||
}
|
||||
if (historyProcess) {
|
||||
if (auto host = historyController(historyProcess)) {
|
||||
AllowSetForegroundWindow(GetProcessId(historyProcess));
|
||||
PostMessageW(host, HistoryHostShow, 0, 0);
|
||||
}
|
||||
return;
|
||||
}
|
||||
auto path = executable();
|
||||
auto command = L"\"" + path + L"\" --history-host=" + std::to_wstring(reinterpret_cast<uintptr_t>(window));
|
||||
STARTUPINFOW startup{sizeof(startup)};
|
||||
startup.dwFlags = STARTF_USESHOWWINDOW; startup.wShowWindow = SW_HIDE;
|
||||
PROCESS_INFORMATION child{};
|
||||
if (!CreateProcessW(path.c_str(), command.data(), nullptr, nullptr, FALSE, CREATE_NO_WINDOW,
|
||||
nullptr, std::filesystem::path(path).parent_path().c_str(), &startup, &child))
|
||||
throw std::runtime_error("Cannot start clipboard history window");
|
||||
CloseHandle(child.hThread);
|
||||
historyProcess = child.hProcess;
|
||||
AllowSetForegroundWindow(child.dwProcessId);
|
||||
}
|
||||
void App::refreshHistoryProcess()
|
||||
{
|
||||
if (auto host = historyController(historyProcess)) PostMessageW(host, HistoryHostRefresh, 0, 0);
|
||||
}
|
||||
void App::remoteHistory(const char *action, const std::wstring &id, bool plain)
|
||||
{
|
||||
if (!historyService || !configurationPeer(historyService, L"KSwitcher.exe"))
|
||||
throw std::runtime_error("KSwitcher has stopped. Reopen clipboard history.");
|
||||
auto text = HistoryJson{{"action", action}, {"id", utf8(id)}, {"plain", plain}}.dump();
|
||||
if (std::string_view(action) == "paste") {
|
||||
DWORD residentPid{}; GetWindowThreadProcessId(historyService, &residentPid);
|
||||
AllowSetForegroundWindow(residentPid);
|
||||
}
|
||||
COPYDATASTRUCT message{HistoryRequest, DWORD(text.size()), text.data()};
|
||||
DWORD_PTR result{};
|
||||
if (!SendMessageTimeoutW(historyService, WM_COPYDATA, reinterpret_cast<WPARAM>(window),
|
||||
reinterpret_cast<LPARAM>(&message), SMTO_ABORTIFHUNG | SMTO_ERRORONEXIT,
|
||||
15000, &result) || result != 1)
|
||||
throw std::runtime_error("KSwitcher could not complete the history request. Reopen history and retry.");
|
||||
}
|
||||
LRESULT App::historyRequest(HWND sender, const COPYDATASTRUCT &message)
|
||||
{
|
||||
DWORD pid{}; GetWindowThreadProcessId(sender, &pid);
|
||||
if (!historyProcess || pid != GetProcessId(historyProcess) ||
|
||||
!configurationPeer(sender, L"KSwitcher.exe") || message.cbData > 4096 ||
|
||||
!message.cbData || !message.lpData || installingUpdate) return FALSE;
|
||||
try {
|
||||
const auto *first = static_cast<const char *>(message.lpData);
|
||||
auto job = HistoryJson::parse(first, first + message.cbData);
|
||||
auto action = job.at("action").get<std::string>();
|
||||
auto id = wide(job.at("id").get<std::string>());
|
||||
if (id.size() > 128) return FALSE;
|
||||
if (action == "ready") {
|
||||
auto ui = reinterpret_cast<HWND>(uintptr_t(std::stoull(id)));
|
||||
DWORD uiPid{}; GetWindowThreadProcessId(ui, &uiPid);
|
||||
wchar_t name[64]{}; GetClassNameW(ui, name, 64);
|
||||
if (uiPid != pid || std::wstring(name) != L"KSwitcher.History") return FALSE;
|
||||
historyWindow = ui;
|
||||
} else if (action == "paste") {
|
||||
if (busy || !history) return FALSE;
|
||||
pasteEntry(id, job.at("plain").get<bool>());
|
||||
} else if (action == "ocr") {
|
||||
if (busy || !history) return FALSE;
|
||||
recognizeEntry(id);
|
||||
} else if (action == "clear") clearHistory();
|
||||
else if (action == "pin" || action == "remove") {
|
||||
if (busy || !history) return FALSE;
|
||||
if (action == "pin") history->pin(id); else history->remove(id);
|
||||
} else return FALSE;
|
||||
return TRUE;
|
||||
} catch (...) { return FALSE; }
|
||||
}
|
||||
void App::saveHistoryView(const std::wstring &selection, const std::wstring &top, const std::wstring &query)
|
||||
{
|
||||
if (!historyService) return;
|
||||
try {
|
||||
auto text = HistoryJson{{"selection", utf8(selection)}, {"top", utf8(top)}, {"query", utf8(query)}}.dump();
|
||||
atomicWrite(data / L"history-view.bin", protect(Bytes(text.begin(), text.end())));
|
||||
} catch (...) {} // Closing the UI must still release its process if state cannot be saved.
|
||||
}
|
||||
+1
-1
@@ -79,7 +79,7 @@ LRESULT CALLBACK controller(HWND window, UINT message, WPARAM wp, LPARAM lp)
|
||||
}
|
||||
// The configurator owns no history store, dictionaries, worker or input hooks.
|
||||
// All mutations are validated and committed by the resident process.
|
||||
App::App(HINSTANCE h, bool, const std::filesystem::path &) : instance(h)
|
||||
App::App(HINSTANCE h, bool, const std::filesystem::path &, HWND) : instance(h)
|
||||
{
|
||||
current = this;
|
||||
data = dataDirectory();
|
||||
|
||||
@@ -1,12 +1,27 @@
|
||||
#include "app.h"
|
||||
#include <shellapi.h>
|
||||
#include <shlobj.h>
|
||||
#include "configuration.h"
|
||||
int WINAPI wWinMain(HINSTANCE instance, HINSTANCE, PWSTR, int)
|
||||
{
|
||||
CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED);
|
||||
int argc{};
|
||||
auto argv = CommandLineToArgvW(GetCommandLineW(), &argc);
|
||||
std::wstring arg = argc > 1 ? argv[1] : L"";
|
||||
if (argc == 2 && arg.starts_with(L"--history-host=")) {
|
||||
LocalFree(argv);
|
||||
int result = 1;
|
||||
try {
|
||||
auto service = reinterpret_cast<HWND>(uintptr_t(std::stoull(arg.substr(15))));
|
||||
wchar_t name[64]{}; GetClassNameW(service, name, 64);
|
||||
if (std::wstring(name) != L"KSwitcher.Main" || !ks::configurationPeer(service, L"KSwitcher.exe"))
|
||||
throw std::runtime_error("Invalid history service");
|
||||
ks::App app(instance, true, {}, service);
|
||||
result = app.execute();
|
||||
} catch (...) {}
|
||||
CoUninitialize();
|
||||
return result;
|
||||
}
|
||||
if (arg.starts_with(L"--configure-autostart="))
|
||||
{
|
||||
int result = 1;
|
||||
@@ -72,6 +87,7 @@ int WINAPI wWinMain(HINSTANCE instance, HINSTANCE, PWSTR, int)
|
||||
try
|
||||
{
|
||||
ks::App app(instance, arg == L"--smoke-test");
|
||||
app.isolateHistory = !app.smoke;
|
||||
if (arg.starts_with(L"--update-ready-event="))
|
||||
app.updateStartupEvent = arg.substr(std::wstring(L"--update-ready-event=").size());
|
||||
else if (!arg.empty() && arg != L"--smoke-test")
|
||||
|
||||
+17
-4
@@ -265,15 +265,27 @@ std::wstring processName(HWND window)
|
||||
CloseHandle(h);
|
||||
return ok ? std::filesystem::path(std::wstring(path, n)).stem().wstring() : L"";
|
||||
}
|
||||
Clip readClipboard(HWND window, size_t limit)
|
||||
Clip readClipboard(HWND window, size_t limit, ClipboardRead mode, std::optional<DWORD> expected)
|
||||
{
|
||||
ClipboardLock lock(window);
|
||||
if (expected && *expected != GetClipboardSequenceNumber())
|
||||
throw std::runtime_error("Clipboard changed while taking snapshot");
|
||||
if (mode == ClipboardRead::WithoutImages &&
|
||||
(IsClipboardFormatAvailable(CF_DIB) || IsClipboardFormatAvailable(CF_DIBV5) ||
|
||||
IsClipboardFormatAvailable(CF_BITMAP) || IsClipboardFormatAvailable(RegisterClipboardFormatW(L"PNG"))))
|
||||
throw std::runtime_error("Clipboard changed to an image");
|
||||
Clip c;
|
||||
size_t total = 0;
|
||||
for (UINT f = EnumClipboardFormats(0); f; f = EnumClipboardFormats(f))
|
||||
{
|
||||
if (!globalFormat(f))
|
||||
continue;
|
||||
wchar_t name[256]{};
|
||||
if (f >= 0xc000) GetClipboardFormatNameW(f, name, 256);
|
||||
if (mode == ClipboardRead::Selection && f != CF_UNICODETEXT &&
|
||||
std::wstring_view(name) != L"VisualStudioEditorOperationsLineCutCopyClipboardTag" &&
|
||||
std::wstring_view(name) != L"MSDEVLineSelect" && std::wstring_view(name) != L"vscode-editor-data")
|
||||
continue;
|
||||
HANDLE handle = GetClipboardData(f);
|
||||
if (!handle)
|
||||
continue;
|
||||
@@ -291,8 +303,6 @@ Clip readClipboard(HWND window, size_t limit)
|
||||
GlobalUnlock(handle);
|
||||
if (f >= 0xc000)
|
||||
{
|
||||
wchar_t name[256]{};
|
||||
GetClipboardFormatNameW(f, name, 256);
|
||||
format.name = name;
|
||||
}
|
||||
total += size;
|
||||
@@ -300,7 +310,8 @@ Clip readClipboard(HWND window, size_t limit)
|
||||
}
|
||||
return c;
|
||||
}
|
||||
DWORD writeClipboard(HWND window, const Clip &c)
|
||||
DWORD writeClipboard(HWND window, const Clip &c) { return writeClipboardGuarded(window, c, {}); }
|
||||
DWORD writeClipboardGuarded(HWND window, const Clip &c, std::optional<DWORD> expected)
|
||||
{
|
||||
// Allocate all blocks before replacing any existing data.
|
||||
std::vector<std::pair<UINT, HGLOBAL>> blocks;
|
||||
@@ -327,6 +338,8 @@ DWORD writeClipboard(HWND window, const Clip &c)
|
||||
blocks.emplace_back(id, h);
|
||||
}
|
||||
ClipboardLock lock(window);
|
||||
if (expected && *expected != GetClipboardSequenceNumber())
|
||||
throw std::runtime_error("Clipboard changed before restoring snapshot");
|
||||
if (!EmptyClipboard())
|
||||
throw std::runtime_error("Cannot replace clipboard");
|
||||
for (auto &[id, h] : blocks)
|
||||
|
||||
+4
-1
@@ -43,8 +43,11 @@ bool ownsClipboard(HWND);
|
||||
bool webViewDescendant(DWORD owner, DWORD host,
|
||||
const std::function<std::optional<std::pair<DWORD, std::wstring>>(DWORD)> &read);
|
||||
std::wstring processName(HWND);
|
||||
Clip readClipboard(HWND, size_t limit = 32 * 1024 * 1024);
|
||||
enum class ClipboardRead { All, Selection, WithoutImages };
|
||||
Clip readClipboard(HWND, size_t limit = 32 * 1024 * 1024,
|
||||
ClipboardRead = ClipboardRead::All, std::optional<DWORD> expected = {});
|
||||
DWORD writeClipboard(HWND, const Clip &);
|
||||
DWORD writeClipboardGuarded(HWND, const Clip &, std::optional<DWORD> expected);
|
||||
Clip textClip(const std::wstring &);
|
||||
bool chord(WORD key, UINT modifiers = 0);
|
||||
// Process-local marker, shared by every KSwitcher input producer (not an authentication token).
|
||||
|
||||
@@ -50,6 +50,10 @@ int WINAPI wWinMain(HINSTANCE instance,HINSTANCE,PWSTR,int)
|
||||
std::wstring token;
|
||||
bool autoOpenEditor=false, writeLog=false;
|
||||
int argc{}; auto argv=CommandLineToArgvW(GetCommandLineW(),&argc);
|
||||
if(argc==3 && std::wstring(argv[1])==L"--clipboard-snapshot") {
|
||||
std::filesystem::path request=argv[2]; LocalFree(argv);
|
||||
return ks::runClipboardSnapshot(request);
|
||||
}
|
||||
if(argc==3 && (std::wstring(argv[1])==L"--capture-history" || std::wstring(argv[1])==L"--history-ocr")) {
|
||||
bool ocr=std::wstring(argv[1])==L"--history-ocr";
|
||||
std::filesystem::path request=argv[2]; LocalFree(argv);
|
||||
|
||||
+112
-8
@@ -410,6 +410,87 @@ void History::loadIndex()
|
||||
}
|
||||
entries_ = std::move(loaded);
|
||||
}
|
||||
void History::reload() { std::lock_guard lock(mutex_); loadIndex(); }
|
||||
void ClipboardSnapshot::cleanup() noexcept
|
||||
{
|
||||
if (request_.empty()) return;
|
||||
std::error_code ignored;
|
||||
for (const auto *suffix : {L"", L".snapshot", L".result"})
|
||||
std::filesystem::remove(std::filesystem::path(request_.wstring() + suffix), ignored);
|
||||
request_.clear();
|
||||
}
|
||||
ClipboardSnapshot::~ClipboardSnapshot() { cleanup(); }
|
||||
DWORD ClipboardSnapshot::run(bool restore, DWORD sequence)
|
||||
{
|
||||
atomicWrite(request_, protect(jsonBytes(Json{{"schema", 1}, {"restore", restore}, {"sequence", sequence}})));
|
||||
auto helper = std::filesystem::path(executable()).parent_path() / L"KSwitcher.Screenshot.exe";
|
||||
auto command = L"\"" + helper.wstring() + L"\" --clipboard-snapshot \"" + request_.wstring() + L"\"";
|
||||
STARTUPINFOW startup{sizeof(startup)};
|
||||
startup.dwFlags = STARTF_USESHOWWINDOW; startup.wShowWindow = SW_HIDE;
|
||||
PROCESS_INFORMATION process{};
|
||||
if (!CreateProcessW(helper.c_str(), command.data(), nullptr, nullptr, FALSE, CREATE_NO_WINDOW,
|
||||
nullptr, helper.parent_path().c_str(), &startup, &process))
|
||||
throw std::runtime_error("Cannot start clipboard snapshot helper");
|
||||
CloseHandle(process.hThread);
|
||||
auto wait = WaitForSingleObject(process.hProcess, 10000);
|
||||
DWORD result = ERROR_GEN_FAILURE;
|
||||
if (wait != WAIT_OBJECT_0)
|
||||
{
|
||||
TerminateProcess(process.hProcess, ERROR_TIMEOUT);
|
||||
WaitForSingleObject(process.hProcess, INFINITE);
|
||||
}
|
||||
GetExitCodeProcess(process.hProcess, &result);
|
||||
CloseHandle(process.hProcess);
|
||||
if (wait != WAIT_OBJECT_0 || result) throw std::runtime_error("Clipboard snapshot helper failed");
|
||||
return parse(unprotect(readFile(request_.wstring() + L".result", 4096))).at("sequence").get<DWORD>();
|
||||
}
|
||||
void ClipboardSnapshot::capture(HWND window, DWORD sequence)
|
||||
{
|
||||
cleanup();
|
||||
clip_ = {};
|
||||
if (!IsClipboardFormatAvailable(CF_DIB) && !IsClipboardFormatAvailable(CF_DIBV5) &&
|
||||
!IsClipboardFormatAvailable(CF_BITMAP) && !IsClipboardFormatAvailable(RegisterClipboardFormatW(L"PNG")))
|
||||
{
|
||||
clip_ = readClipboard(window, 64 * 1024 * 1024, ClipboardRead::WithoutImages, sequence);
|
||||
return;
|
||||
}
|
||||
request_ = std::filesystem::temp_directory_path() / (L"ks-clipboard-" + newSnippetId() + L".request");
|
||||
try { run(false, sequence); }
|
||||
catch (...) { cleanup(); throw; }
|
||||
}
|
||||
DWORD ClipboardSnapshot::restore(HWND window, DWORD sequence)
|
||||
{
|
||||
return request_.empty() ? writeClipboardGuarded(window, clip_, sequence) : run(true, sequence);
|
||||
}
|
||||
int runClipboardSnapshot(const std::filesystem::path &request)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!request.is_absolute() || !request.filename().wstring().starts_with(L"ks-clipboard-") ||
|
||||
request.extension() != L".request") return 1;
|
||||
auto job = parse(unprotect(readFile(request, 4096)));
|
||||
if (job.at("schema") != 1) return 1;
|
||||
auto sequence = job.at("sequence").get<DWORD>();
|
||||
const auto snapshot = request.wstring() + L".snapshot";
|
||||
if (job.at("restore").get<bool>())
|
||||
{
|
||||
auto clip = decodeClip(unprotect(readFile(snapshot, 70 * 1024 * 1024)));
|
||||
HWND window = CreateWindowExW(0, L"STATIC", L"", 0, 0, 0, 0, 0, HWND_MESSAGE, nullptr, nullptr, nullptr);
|
||||
if (!window) return 1;
|
||||
struct WindowCleanup { HWND window; ~WindowCleanup() { DestroyWindow(window); } } cleanup{window};
|
||||
sequence = writeClipboardGuarded(window, clip, sequence);
|
||||
}
|
||||
else
|
||||
{
|
||||
auto clip = readClipboard(nullptr, 64 * 1024 * 1024, ClipboardRead::All, sequence);
|
||||
if (sequence != GetClipboardSequenceNumber()) return 2;
|
||||
atomicWrite(snapshot, protect(encodeClip(clip)));
|
||||
}
|
||||
atomicWrite(request.wstring() + L".result", protect(jsonBytes(Json{{"sequence", sequence}})));
|
||||
return 0;
|
||||
}
|
||||
catch (...) { return 1; }
|
||||
}
|
||||
bool History::captureImage(DWORD sequence, const std::wstring &app, const Settings &settings)
|
||||
{
|
||||
// Serialize helper index changes with all resident history mutations. Only
|
||||
@@ -706,12 +787,12 @@ void History::clear()
|
||||
std::filesystem::remove(folder_ / (e.id + L".ocr"), ec);
|
||||
}
|
||||
}
|
||||
std::wstring History::recognizeImage(const std::wstring &id, const Settings &settings,
|
||||
const std::function<bool()> &cancelled) const
|
||||
static Bytes historyImageJob(const std::filesystem::path &folder, const std::wstring &id,
|
||||
const Json &job, const std::function<bool()> &cancelled)
|
||||
{
|
||||
if (!validId(id)) throw std::runtime_error("Invalid history ID");
|
||||
if (cancelled && cancelled()) throw std::runtime_error("History OCR cancelled");
|
||||
auto request = std::filesystem::absolute(folder_) / (L"ocr-" + newSnippetId() + L".request");
|
||||
auto request = std::filesystem::absolute(folder) / (L"ocr-" + newSnippetId() + L".request");
|
||||
auto response = request; response += L".result";
|
||||
struct Cleanup {
|
||||
std::filesystem::path request, response;
|
||||
@@ -721,9 +802,6 @@ std::wstring History::recognizeImage(const std::wstring &id, const Settings &set
|
||||
std::filesystem::remove(response, ignored);
|
||||
}
|
||||
} cleanup{request, response};
|
||||
// Send only OCR options and the entry ID. The resident never loads image bytes.
|
||||
Json job{{"schema", 1}, {"id", utf8(id)}, {"tesseract", settings.useTesseract},
|
||||
{"path", utf8(settings.tesseractPath)}, {"languages", utf8(settings.tesseractLanguages)}};
|
||||
atomicWrite(request, protect(jsonBytes(job)));
|
||||
auto helper = std::filesystem::path(executable()).parent_path() / L"KSwitcher.Screenshot.exe";
|
||||
auto command = L"\"" + helper.wstring() + L"\" --history-ocr \"" + request.wstring() + L"\"";
|
||||
@@ -755,11 +833,28 @@ std::wstring History::recognizeImage(const std::wstring &id, const Settings &set
|
||||
DWORD result{};
|
||||
if (!GetExitCodeProcess(child.handle, &result) || result)
|
||||
throw std::runtime_error("History OCR helper failed: " + std::to_string(result));
|
||||
auto reply = parse(unprotect(readFile(response, 8 * 1024 * 1024)));
|
||||
return unprotect(readFile(response, 8 * 1024 * 1024));
|
||||
}
|
||||
std::wstring History::recognizeImage(const std::wstring &id, const Settings &settings,
|
||||
const std::function<bool()> &cancelled) const
|
||||
{
|
||||
Json job{{"schema", 1}, {"id", utf8(id)}, {"tesseract", settings.useTesseract},
|
||||
{"path", utf8(settings.tesseractPath)}, {"languages", utf8(settings.tesseractLanguages)}};
|
||||
auto reply = parse(historyImageJob(folder_, id, job, cancelled));
|
||||
if (reply.at("schema") != 1 || str(reply, "id") != id)
|
||||
throw std::runtime_error("Invalid history OCR response");
|
||||
return str(reply, "text");
|
||||
}
|
||||
Pixels History::thumbnail(const std::wstring &id, const std::function<bool()> &cancelled) const
|
||||
{
|
||||
auto bytes = historyImageJob(folder_, id, Json{{"schema", 1}, {"id", utf8(id)}, {"thumbnail", true}}, cancelled);
|
||||
size_t at = 0;
|
||||
auto width = get32(bytes, at), height = get32(bytes, at);
|
||||
if (!width || !height || width > 208 || height > 128 || bytes.size() - at != width * height * 4)
|
||||
throw std::runtime_error("Invalid history thumbnail response");
|
||||
bytes.erase(bytes.begin(), bytes.begin() + at);
|
||||
return Pixels{width, height, std::move(bytes)};
|
||||
}
|
||||
int runHistoryOcr(const std::filesystem::path &request)
|
||||
{
|
||||
try {
|
||||
@@ -768,11 +863,20 @@ int runHistoryOcr(const std::filesystem::path &request)
|
||||
auto job = parse(unprotect(readFile(request, 1024 * 1024)));
|
||||
auto id = str(job, "id");
|
||||
if (job.at("schema") != 1 || !validId(id)) return 1;
|
||||
auto clip = decodeClip(unprotect(readFile(request.parent_path() / (id + L".dat"), 80 * 1024 * 1024)));
|
||||
if (job.value("thumbnail", false)) {
|
||||
auto pixels = decodeImage(clip, 208, 128);
|
||||
if (!pixels.width || !pixels.height) return 1;
|
||||
Bytes reply;
|
||||
put32(reply, pixels.width); put32(reply, pixels.height);
|
||||
reply.insert(reply.end(), pixels.bgra.begin(), pixels.bgra.end());
|
||||
atomicWrite(request.wstring() + L".result", protect(reply));
|
||||
return 0;
|
||||
}
|
||||
Settings settings;
|
||||
settings.useTesseract = job.at("tesseract").get<bool>();
|
||||
settings.tesseractPath = str(job, "path");
|
||||
settings.tesseractLanguages = str(job, "languages");
|
||||
auto clip = decodeClip(unprotect(readFile(request.parent_path() / (id + L".dat"), 80 * 1024 * 1024)));
|
||||
auto text = utf8(recognizeText(clip, settings));
|
||||
if (text.size() > 1024 * 1024) return 1;
|
||||
auto response = request; response += L".result";
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
#include <functional>
|
||||
namespace ks
|
||||
{
|
||||
struct Pixels;
|
||||
Settings loadSettings(const std::filesystem::path &, const std::vector<Layout> &);
|
||||
Settings decodeSettings(const Bytes &, const std::vector<Layout> &);
|
||||
Bytes encodeSettings(const Settings &);
|
||||
@@ -29,12 +30,14 @@ class History
|
||||
|
||||
public:
|
||||
explicit History(const std::filesystem::path &);
|
||||
void reload();
|
||||
std::vector<Entry> entries() const;
|
||||
Clip load(const std::wstring &id) const;
|
||||
void add(const Clip &, const std::wstring &app, const Settings &);
|
||||
bool captureImage(DWORD sequence, const std::wstring &app, const Settings &);
|
||||
std::wstring recognizeImage(const std::wstring &id, const Settings &,
|
||||
const std::function<bool()> &cancelled = {}) const;
|
||||
Pixels thumbnail(const std::wstring &id, const std::function<bool()> &cancelled = {}) const;
|
||||
bool saveOcr(const std::wstring &id, const std::optional<std::wstring> &, const Settings &);
|
||||
std::optional<std::wstring> ocrText(const std::wstring &id, const std::wstring &profile = L"") const;
|
||||
void remove(const std::wstring &);
|
||||
@@ -47,4 +50,20 @@ Clip decodeClip(const Bytes &);
|
||||
bool sameClipboardImage(const Clip &, const Clip &);
|
||||
int runImageHistoryCapture(const std::filesystem::path &request);
|
||||
int runHistoryOcr(const std::filesystem::path &request);
|
||||
// Image backups stay encrypted on disk and are read/written only by a short-lived helper.
|
||||
class ClipboardSnapshot
|
||||
{
|
||||
Clip clip_;
|
||||
std::filesystem::path request_;
|
||||
void cleanup() noexcept;
|
||||
DWORD run(bool restore, DWORD sequence);
|
||||
public:
|
||||
ClipboardSnapshot() = default;
|
||||
ClipboardSnapshot(const ClipboardSnapshot &) = delete;
|
||||
ClipboardSnapshot &operator=(const ClipboardSnapshot &) = delete;
|
||||
~ClipboardSnapshot();
|
||||
void capture(HWND, DWORD sequence);
|
||||
DWORD restore(HWND, DWORD sequence);
|
||||
};
|
||||
int runClipboardSnapshot(const std::filesystem::path &request);
|
||||
} // namespace ks
|
||||
|
||||
+26
-5
@@ -549,11 +549,11 @@ void requestPreview(HWND w, HistoryView *v, const Entry &entry)
|
||||
auto result = new PreviewResult{generation, id, {}, {}};
|
||||
try
|
||||
{
|
||||
auto clip = app->history->load(id);
|
||||
if (!active->load()) { delete result; return; }
|
||||
if (image)
|
||||
result->pixels = decodeImage(clip, 208, 128);
|
||||
result->pixels = app->history->thumbnail(id, [active] { return !active->load(); });
|
||||
else {
|
||||
auto clip = app->history->load(id);
|
||||
if (!active->load()) { delete result; return; }
|
||||
result->text.rtf = richPreview(clip, false);
|
||||
if (result->text.rtf.empty()) {
|
||||
result->text.plain = clip.text();
|
||||
@@ -771,7 +771,10 @@ LRESULT CALLBACK historyProc(HWND w, UINT msg, WPARAM wp, LPARAM lp)
|
||||
{
|
||||
if (!entry.empty())
|
||||
{
|
||||
if (id == 112)
|
||||
if (v->app->historyService) {
|
||||
v->app->remoteHistory(id == 112 ? "pin" : "remove", entry);
|
||||
v->app->history->reload();
|
||||
} else if (id == 112)
|
||||
v->app->history->pin(entry);
|
||||
else
|
||||
v->app->history->remove(entry);
|
||||
@@ -907,6 +910,7 @@ LRESULT CALLBACK historyProc(HWND w, UINT msg, WPARAM wp, LPARAM lp)
|
||||
v->rememberedSelection = selected(w, v);
|
||||
int top = (int)SendDlgItemMessageW(w,102,LB_GETTOPINDEX,0,0);
|
||||
v->rememberedTop = top >= 0 && top < (int)v->items.size() ? v->items[top].id : L"";
|
||||
v->app->saveHistoryView(v->rememberedSelection, v->rememberedTop, value(w, 101));
|
||||
KillTimer(w, 1);
|
||||
v->closed = true;
|
||||
v->previewActive->store(false);
|
||||
@@ -918,6 +922,8 @@ LRESULT CALLBACK historyProc(HWND w, UINT msg, WPARAM wp, LPARAM lp)
|
||||
v->richTexts.clear();
|
||||
if (v->rich) SetWindowTextW(v->rich, L"");
|
||||
v->pending.clear();
|
||||
if (v->app->historyService && !v->app->historyEntryWindows)
|
||||
PostMessageW(v->app->window, WM_CLOSE, 0, 0);
|
||||
return 0;
|
||||
}
|
||||
case WM_NCDESTROY:
|
||||
@@ -1681,13 +1687,19 @@ LRESULT CALLBACK snippetProc(HWND w, UINT msg, WPARAM wp, LPARAM lp)
|
||||
#ifndef KS_CONFIG_APP
|
||||
LRESULT CALLBACK entryProc(HWND w, UINT msg, WPARAM wp, LPARAM lp)
|
||||
{
|
||||
if (msg == WM_NCCREATE)
|
||||
SetWindowLongPtrW(w, GWLP_USERDATA, (LONG_PTR)((CREATESTRUCTW *)lp)->lpCreateParams);
|
||||
if (msg == WM_SIZE)
|
||||
{
|
||||
MoveWindow(GetDlgItem(w, 1), 10, 10, LOWORD(lp) - 20, HIWORD(lp) - 20, TRUE);
|
||||
return 0;
|
||||
}
|
||||
if (msg == WM_NCDESTROY)
|
||||
if (msg == WM_NCDESTROY) {
|
||||
disposeFont(w);
|
||||
auto *app = (App *)GetWindowLongPtrW(w, GWLP_USERDATA);
|
||||
if (app && app->historyService && app->historyEntryWindows && !--app->historyEntryWindows &&
|
||||
!IsWindowVisible(app->historyWindow)) PostMessageW(app->window, WM_CLOSE, 0, 0);
|
||||
}
|
||||
return DefWindowProcW(w, msg, wp, lp);
|
||||
}
|
||||
#endif
|
||||
@@ -1695,6 +1707,7 @@ LRESULT CALLBACK entryProc(HWND w, UINT msg, WPARAM wp, LPARAM lp)
|
||||
#ifndef KS_CONFIG_APP
|
||||
void refreshHistory(App &app)
|
||||
{
|
||||
if (app.isolateHistory) { app.refreshHistoryProcess(); return; }
|
||||
if (app.historyWindow)
|
||||
{
|
||||
auto *v = (HistoryView *)GetWindowLongPtrW(app.historyWindow, GWLP_USERDATA);
|
||||
@@ -1703,6 +1716,7 @@ void refreshHistory(App &app)
|
||||
}
|
||||
void showHistory(App &app, bool visible)
|
||||
{
|
||||
if (app.isolateHistory) { if (visible) app.openHistoryProcess(); return; }
|
||||
if (!app.history)
|
||||
{
|
||||
app.report(L"Clipboard history is unavailable. See the log in the data folder.");
|
||||
@@ -1738,6 +1752,12 @@ void showHistory(App &app, bool visible)
|
||||
button(w, L"Clear &history…", 116, 546, 452, 150);
|
||||
SetWindowSubclass(list, historyKey, 1, (DWORD_PTR)w);
|
||||
SetWindowSubclass(GetDlgItem(w, 101), historyKey, 1, (DWORD_PTR)w);
|
||||
if (app.historyService) {
|
||||
auto *v = (HistoryView *)GetWindowLongPtrW(w, GWLP_USERDATA);
|
||||
v->rememberedSelection = app.historySelection;
|
||||
v->rememberedTop = app.historyTop;
|
||||
SetDlgItemTextW(w, 101, app.historyQuery.c_str());
|
||||
}
|
||||
}
|
||||
if (!visible) return;
|
||||
POINT cursor;
|
||||
@@ -1894,6 +1914,7 @@ void showEntry(App &app, const std::wstring &id)
|
||||
}
|
||||
HWND w = makeWindow(app, L"KSwitcher.Entry", L"Clipboard item", entryProc, 720, 500,
|
||||
WS_THICKFRAME | WS_MAXIMIZEBOX);
|
||||
if (app.historyService) ++app.historyEntryWindows;
|
||||
auto field = edit(w, 1, 10, 10, 700, 480, ES_MULTILINE | ES_READONLY | ES_AUTOVSCROLL | WS_VSCROLL);
|
||||
SetWindowTextW(field, text.c_str());
|
||||
showRequestedWindow(w);
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@
|
||||
#include "platform.h"
|
||||
namespace ks
|
||||
{
|
||||
inline constexpr wchar_t AppVersion[] = L"0.6.38";
|
||||
inline constexpr wchar_t AppVersion[] = L"0.6.39";
|
||||
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