Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 94cab4c112 |
+1
-1
@@ -1,5 +1,5 @@
|
||||
cmake_minimum_required(VERSION 3.24)
|
||||
project(KSwitcher VERSION 0.6.35 LANGUAGES CXX RC)
|
||||
project(KSwitcher VERSION 0.6.36 LANGUAGES CXX RC)
|
||||
if(NOT WIN32)
|
||||
message(FATAL_ERROR "KSwitcher uses native Windows APIs")
|
||||
endif()
|
||||
|
||||
@@ -707,3 +707,7 @@ supporting old clients; their immutable update code only reads that page.
|
||||
### Editors that omit copied trailing spaces
|
||||
|
||||
From v0.6.31, KSwitcher recovers trailing ordinary spaces omitted by Ctrl+C only when Windows accessibility proves the complete selection. The same focused control and range are rechecked before paste, and the result is read back even without Write log. Recent-text conversion, automatic correction, repeat conversion and Undo retain exact verification. Missing letters, tabs, line breaks and unavailable or ambiguous selections are not reconstructed. No setting or shortcut has changed.
|
||||
|
||||
### Notification registration and Viber — v0.6.36
|
||||
|
||||
Notification registration runs in KSwitcher.Screenshot.exe; keep all three executables together. Startup registration is asynchronous. Viber pastes allow 75 ms for clipboard notifications before Ctrl+V, retaining focus, ownership and selection checks. This mitigates an intermittent clipboard timing issue; it does not guarantee all Viber failures are resolved. UI Automation is unchanged.
|
||||
|
||||
Binary file not shown.
+8
-9
@@ -1,18 +1,17 @@
|
||||
# Latest Windows release
|
||||
|
||||
## v0.6.35 — 2026-11-04
|
||||
## v0.6.36 — 2026-11-04
|
||||
|
||||
## Changes
|
||||
- Reduced resident memory: the input predictor and correction worker share immutable dictionaries. Compact indexed blocks retain all 140,939 bundled words and their frequency ranks while reducing the EN/UK/RU lookup payload from approximately 3.06 MiB per copy to 0.90 MiB shared. Custom UTF-8 dictionary files, duplicate handling, frequency limits and personal dictionaries remain compatible.
|
||||
- Closing clipboard history releases its duplicate row list and Windows list strings, retains selection/scroll identifiers for reopening, and rejects late search results and timers. Preview caches now have payload budgets of 1 MiB for thumbnails and 256 KiB for formatted text, plus the existing item-count limit. Visible entries are retained even if an unusually large viewport exceeds a budget.
|
||||
- PNG decoding reads the existing input buffer without making another full PNG copy. DIB decoding allocates its temporary BMP buffer once at the required size.
|
||||
- Idle maintenance releases diagnostic context older than 30 seconds and expired Undo text after its existing two-minute eligibility window, without interrupting an active operation. Completed sync buffers are released on success, failure and stale responses; unresolved conflicts remain available.
|
||||
- No shortcut, default, on-disk history format or screenshot-editing behavior changed. The v0.6.34 four-direction movement cursors remain included.
|
||||
- Notification registration now runs in the existing KSwitcher.Screenshot.exe helper. Startup launches it asynchronously and checks its exit status without blocking keyboard work; update notification registration waits in its background worker. Keep all three executables together. UI Automation and notification activation routes remain unchanged.
|
||||
- Viber paste now allows 75 ms for clipboard notifications to settle before sending Ctrl+V, then rechecks input, focus and clipboard ownership. This mitigates a suspected clipboard timing race: an intermittent corrupted Cyrillic paste was reproduced on the prior build. It is not a guarantee that every Viber timing issue is resolved; no blind paste retries or relaxed selection checks were added.
|
||||
- Added regression coverage for an explicitly selected middle range followed by a repeated shortcut without selection, preserving surrounding text, trailing spaces and the original clipboard. Updated the integration fixture to initialize shared predictor dictionaries.
|
||||
- Added reproducible standalone COM, UI Automation and Shell memory measurement tools. No shortcuts, settings or defaults changed.
|
||||
|
||||
## Validation
|
||||
The full `build.ps1 -Stage -Test` suite covers complete bundled dictionary word/rank equivalence, malformed/custom dictionary boundaries, 105 sync checks, shared wheel batching, 304 native regressions, 559 input/UI checks, screenshot editor UI and PNG saving, isolated screenshot registration, 48 updater regressions and updater progress UI checks. Cache assertions now run once outside the exception helper rather than being repeated inside it. All three executable versions and all 12 package manifest file sizes/SHA-256 hashes were verified; no personal data is packaged.
|
||||
Full staged build and tests cover dictionary equivalence for all 140,939 words, 105 sync checks, wheel batching, 304 native checks, 559 input checks, screenshot UI/PNG/registration, 48 updater checks and updater progress UI. Conversion integration passed 43 assertions. Eight actual Viber conversions passed on the development build, including two complete long-text layout cycles and selected/repeated middle-word conversion; test messages were not sent.
|
||||
|
||||
Controlled local measurements of the final dictionary step, after equal restart/wait periods with three dictionaries enabled, reduced private resident memory from 7.08 to 4.65 MiB and private committed memory from 8.34 to 5.70 MiB. These compare consecutive development builds, not the published v0.6.34 with the full cumulative change. Randomized lookup tests were approximately 20% faster. Results depend on settings, workload, Windows and allocator state; these are not fixed memory limits. The 0.90 MiB dictionary figure excludes process/allocator overhead.
|
||||
A controlled fresh-launch before/after comparison (five private-working-set samples each, same installation and settings) measured 4.69 → 4.18 MiB, about 0.51 MiB saved. This is one local comparison, not a fixed memory limit or a long-term idle guarantee. The prior compact dictionaries and cache/idle memory reductions remain included.
|
||||
|
||||
## Upgrade
|
||||
Choose **Update** in the tray menu. Legacy v0.3.0–v0.4.1 clients first receive the v0.4.2 compatibility bridge, then the current full release. v0.2.3 and earlier require manual installation.
|
||||
@@ -20,4 +19,4 @@ For manual installation, exit KSwitcher, close screenshot editors and extract th
|
||||
|
||||
Download **KSwitcher-win-x64.zip** with **SHA256SUMS.txt**. Keep all three executables together. The package contains no personal settings, credentials or clipboard history.
|
||||
|
||||
Publish stable `release-vX.Y.Z` tags matching the executable version. Keep `v0.4.2` as the last legacy tag.
|
||||
Publish stable `release-vX.Y.Z` tags matching the executable version. Keep `v0.4.2` as the last legacy tag.
|
||||
@@ -1 +1 @@
|
||||
6e36a582eec59b3cc4f0f9d6b3e05cd699371c05cae30a95c6eae51a838f7bfe KSwitcher-win-x64.zip
|
||||
794d74113bddf13ffee7317cadee9c472bca3b84b3361796db58ff8023f66571 KSwitcher-win-x64.zip
|
||||
|
||||
+3
-3
@@ -9,8 +9,8 @@ IDC_SCREENSHOT_PENCIL CURSOR "icons/lucide/pencil.cur"
|
||||
1 RT_MANIFEST "app.manifest"
|
||||
201 RCDATA "update.ps1"
|
||||
1 VERSIONINFO
|
||||
FILEVERSION 0,6,35,0
|
||||
PRODUCTVERSION 0,6,35,0
|
||||
FILEVERSION 0,6,36,0
|
||||
PRODUCTVERSION 0,6,36,0
|
||||
FILEOS VOS_NT_WINDOWS32
|
||||
FILETYPE VFT_APP
|
||||
BEGIN
|
||||
@@ -20,7 +20,7 @@ BEGIN
|
||||
BEGIN
|
||||
VALUE "FileDescription", "KSwitcher — native keyboard and clipboard utility"
|
||||
VALUE "ProductName", "KSwitcher"
|
||||
VALUE "FileVersion", "0.6.35"
|
||||
VALUE "FileVersion", "0.6.36"
|
||||
END
|
||||
END
|
||||
BLOCK "VarFileInfo"
|
||||
|
||||
+18
-1
@@ -358,6 +358,14 @@ class Transaction
|
||||
if (!valid())
|
||||
return false;
|
||||
write(clip);
|
||||
// Viber's Qt editor observes clipboard changes asynchronously. Give it
|
||||
// a chance to discard the data object from Ctrl+C before requesting paste.
|
||||
// Recheck ownership and input below; never retry a possibly applied paste.
|
||||
if (lower(processName(target)) == L"viber")
|
||||
{
|
||||
Sleep(75);
|
||||
diagnosticEvent(L"Paste: Viber clipboard notification settling interval completed");
|
||||
}
|
||||
for (int i = 0; i < 2; ++i)
|
||||
{
|
||||
check();
|
||||
@@ -433,6 +441,7 @@ App::App(HINSTANCE h, bool test, const std::filesystem::path &dataRoot) : instan
|
||||
}
|
||||
App::~App()
|
||||
{
|
||||
if (notificationRegistration) CloseHandle(notificationRegistration);
|
||||
if (syncWorker.joinable()) syncWorker.join();
|
||||
if (updateWorker.joinable())
|
||||
updateWorker.join();
|
||||
@@ -1893,6 +1902,14 @@ LRESULT CALLBACK App::windowProc(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp)
|
||||
case WM_TIMER:
|
||||
if (wp == 6)
|
||||
{
|
||||
if (app->notificationRegistration && WaitForSingleObject(app->notificationRegistration, 0) == WAIT_OBJECT_0)
|
||||
{
|
||||
DWORD code = ERROR_GEN_FAILURE;
|
||||
GetExitCodeProcess(app->notificationRegistration, &code);
|
||||
if (code) log(L"Screenshot notification registration helper failed: " + std::to_wstring(code));
|
||||
CloseHandle(app->notificationRegistration);
|
||||
app->notificationRegistration = nullptr;
|
||||
}
|
||||
app->releaseIdleMemory(GetTickCount64());
|
||||
app->pollSync();
|
||||
return 0;
|
||||
@@ -2012,7 +2029,7 @@ int App::execute()
|
||||
log(std::wstring(L"Update completed; running version ") + AppVersion);
|
||||
if (startCommand)
|
||||
PostMessageW(window, WM_HOTKEY, startCommand, 0);
|
||||
try { registerScreenshotNotifications(); }
|
||||
try { notificationRegistration = startScreenshotNotificationRegistration(); }
|
||||
catch(const std::exception &e) { log(L"Screenshot notification registration: "+wide(e.what())); }
|
||||
auto screenshotActivation=startScreenshotActivationListener();
|
||||
MSG msg;
|
||||
|
||||
@@ -92,6 +92,7 @@ class App
|
||||
std::condition_variable jobsReady;
|
||||
std::deque<std::function<void()>> jobs;
|
||||
std::thread worker;
|
||||
HANDLE notificationRegistration{};
|
||||
std::thread historySearchWorker;
|
||||
std::condition_variable historySearchReady;
|
||||
std::function<void()> pendingHistorySearch;
|
||||
|
||||
@@ -50,6 +50,30 @@ void registerScreenshotNotifications()
|
||||
// Notify the shell that notification identity/activation registration changed.
|
||||
SHChangeNotify(SHCNE_ASSOCCHANGED,SHCNF_IDLIST,nullptr,nullptr);
|
||||
}
|
||||
HANDLE startScreenshotNotificationRegistration()
|
||||
{
|
||||
auto path=std::filesystem::path(executable()).parent_path()/L"KSwitcher.Screenshot.exe";
|
||||
auto command=L"\""+path.wstring()+L"\" --register-notifications";
|
||||
STARTUPINFOW startup{sizeof(startup)};
|
||||
startup.dwFlags=STARTF_USESHOWWINDOW; startup.wShowWindow=SW_HIDE;
|
||||
PROCESS_INFORMATION process{};
|
||||
if(!CreateProcessW(path.c_str(),command.data(),nullptr,nullptr,FALSE,CREATE_NO_WINDOW,
|
||||
nullptr,path.parent_path().c_str(),&startup,&process))
|
||||
throw std::runtime_error(utf8(lastError(L"Cannot start notification registration helper")));
|
||||
CloseHandle(process.hThread);
|
||||
return process.hProcess;
|
||||
}
|
||||
void registerScreenshotNotificationsInHelper()
|
||||
{
|
||||
auto process=startScreenshotNotificationRegistration();
|
||||
auto wait=WaitForSingleObject(process,10000);
|
||||
DWORD code=ERROR_GEN_FAILURE;
|
||||
bool completed=wait==WAIT_OBJECT_0 && GetExitCodeProcess(process,&code);
|
||||
CloseHandle(process);
|
||||
if(!completed || code)
|
||||
throw std::runtime_error(completed ? "Notification registration helper failed: "+std::to_string(code)
|
||||
: "Notification registration helper did not complete within 10 seconds");
|
||||
}
|
||||
bool validScreenshotToken(const std::wstring &id)
|
||||
{
|
||||
if(id.size()!=36) return false;
|
||||
|
||||
@@ -206,7 +206,7 @@ bool notifyUpdateAvailable(const std::wstring &version)
|
||||
{
|
||||
try {
|
||||
// Reuse the screenshot notification identity, Windows settings and COM activator.
|
||||
registerScreenshotNotifications();
|
||||
registerScreenshotNotificationsInHelper();
|
||||
ComPtr<Notifications::IToastNotificationManagerStatics> manager;
|
||||
String managerClass(L"Windows.UI.Notifications.ToastNotificationManager");
|
||||
check(RoGetActivationFactory(managerClass.value,IID_PPV_ARGS(&manager)));
|
||||
|
||||
@@ -4,6 +4,10 @@ namespace ks {
|
||||
bool notifyScreenshot(const Clip &);
|
||||
std::string screenshotNotificationError();
|
||||
void registerScreenshotNotifications();
|
||||
// Caller owns the returned process handle; the helper exits after registration.
|
||||
HANDLE startScreenshotNotificationRegistration();
|
||||
// For background notification workers only; bounded wait, never on the UI thread.
|
||||
void registerScreenshotNotificationsInHelper();
|
||||
int runScreenshotToastActivator();
|
||||
DWORD startScreenshotActivationListener();
|
||||
bool validScreenshotToken(const std::wstring &);
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@
|
||||
#include "platform.h"
|
||||
namespace ks
|
||||
{
|
||||
inline constexpr wchar_t AppVersion[] = L"0.6.35";
|
||||
inline constexpr wchar_t AppVersion[] = L"0.6.36";
|
||||
inline constexpr UINT UpdateMessage = WM_APP + 7;
|
||||
inline constexpr UINT UpdateCheckIntervalMs = 6 * 60 * 60 * 1000;
|
||||
inline constexpr UINT UpdateNotificationRetryMs = 5 * 60 * 1000;
|
||||
|
||||
+17
-1
@@ -529,6 +529,9 @@ LRESULT CALLBACK procedure(HWND w, UINT msg, WPARAM wp, LPARAM lp)
|
||||
require(text() == L"wrong ", "unrelated selection remains rejected with trimmed clipboard");
|
||||
app->settings.autoCorrect = true;
|
||||
app->settings.autoLanguages = {L"en", L"ru"};
|
||||
// The worker now shares the predictor's immutable dictionaries.
|
||||
// This fixture changes settings directly instead of applySettings.
|
||||
app->autoPredictor.configure(L"dictionaries", app->settings.autoLanguages);
|
||||
app->autoCorrector = ks::AutoCorrector{};
|
||||
ActivateKeyboardLayout(app->layouts[en].handle, 0);
|
||||
prepare(L"ghbdtn ", false);
|
||||
@@ -556,9 +559,22 @@ LRESULT CALLBACK procedure(HWND w, UINT msg, WPARAM wp, LPARAM lp)
|
||||
require(trimmedCopies >= 7, "fixture actually stripped spaces during recovery scenarios");
|
||||
pasteBehavior = 0;
|
||||
trimCopiedSpaces = false;
|
||||
finish();
|
||||
// User-selected middle range, followed by the same command with
|
||||
// no selection: retain both surrounding text and trailing spaces.
|
||||
prepare(L"OLDghbdtn NEXT", false);
|
||||
SendMessageW(field, EM_SETSEL, 3, 11);
|
||||
app->transform(ks::Direct + ru);
|
||||
break;
|
||||
}
|
||||
case 58:
|
||||
require(text() == L"OLDпривет NEXT", "explicit middle selection preserves surrounding text");
|
||||
app->transform(ks::Direct + en);
|
||||
break;
|
||||
case 59:
|
||||
require(text() == L"OLDghbdtn NEXT", "hotkey without selection reconverts only the previous middle range");
|
||||
require(ks::readClipboard(app->window).text() == sentinel, "selected and repeated conversions restore clipboard");
|
||||
finish();
|
||||
break;
|
||||
case 30:
|
||||
case 32:
|
||||
case 34:
|
||||
|
||||
@@ -5,9 +5,11 @@
|
||||
static std::wstring programs;
|
||||
static std::wstring activationExecutable, activationCommand;
|
||||
static int activationCalls{};
|
||||
static DWORD launchFlags{}; static bool inheritsHandles{};
|
||||
static BOOL WINAPI testCreateProcess(LPCWSTR path, LPWSTR command, LPSECURITY_ATTRIBUTES,
|
||||
LPSECURITY_ATTRIBUTES, BOOL, DWORD, LPVOID, LPCWSTR, LPSTARTUPINFOW, LPPROCESS_INFORMATION)
|
||||
LPSECURITY_ATTRIBUTES, BOOL inherit, DWORD flags, LPVOID, LPCWSTR, LPSTARTUPINFOW, LPPROCESS_INFORMATION)
|
||||
{
|
||||
launchFlags=flags; inheritsHandles=inherit;
|
||||
++activationCalls; activationExecutable=path; activationCommand=command;
|
||||
SetLastError(ERROR_CANCELLED); return FALSE; // Inspect activation without starting any program.
|
||||
}
|
||||
@@ -55,6 +57,14 @@ int main()
|
||||
valid=id.vt==VT_CLSID && *id.puuid==ks::ActivatorId; PropVariantClear(&id);
|
||||
if(!valid) throw std::runtime_error("shortcut activator missing");
|
||||
ks::registerScreenshotNotifications(); // Registration is safe to repeat.
|
||||
bool launchFailed=false;
|
||||
try { ks::startScreenshotNotificationRegistration(); }
|
||||
catch(const std::exception &) { launchFailed=true; }
|
||||
if(!launchFailed || activationExecutable!=helper.wstring() ||
|
||||
activationCommand!=L"\""+helper.wstring()+L"\" --register-notifications" ||
|
||||
!(launchFlags&CREATE_NO_WINDOW) || inheritsHandles)
|
||||
throw std::runtime_error("registration must launch the sibling helper and propagate failure");
|
||||
activationCalls=0;
|
||||
ks::Activator activator(helper.wstring(),false);
|
||||
activator.Activate(L"HrynCo.KSwitcher.Screenshot",L"check-update",nullptr,0);
|
||||
auto main=(helper.parent_path()/L"KSwitcher.exe").wstring();
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
param([ValidateRange(1,20)][int]$Runs=3,[ValidateRange(100,10000)][int]$SettleMilliseconds=1500)
|
||||
# Private resident pages are counted externally, so the measurement buffer is
|
||||
# allocated in PowerShell, not in the measured process. Never trim working sets.
|
||||
$ErrorActionPreference='Stop'
|
||||
$root=Split-Path $PSScriptRoot -Parent
|
||||
$output=Join-Path $root 'build/memory-components'
|
||||
New-Item -ItemType Directory -Force $output | Out-Null
|
||||
$exe=Join-Path $output 'probe.exe'
|
||||
& "$root/.tools/w64devkit/bin/g++.exe" -std=c++20 -Os -static -s -DUNICODE -D_UNICODE -D_WIN32_WINNT=0x0A00 "$PSScriptRoot/memory-components.cpp" -o $exe -lole32 -loleaut32 -lshell32 -lpropsys -lshlwapi -luuid -luiautomationcore
|
||||
if($LASTEXITCODE) { throw 'Probe compilation failed' }
|
||||
Add-Type @'
|
||||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
public static class ComponentWorkingSet {
|
||||
[DllImport("psapi.dll", SetLastError=true)] static extern bool QueryWorkingSet(IntPtr process,IntPtr buffer,int size);
|
||||
public static long PrivateBytes(IntPtr process) {
|
||||
int size=4*1024*1024;
|
||||
IntPtr buffer=Marshal.AllocHGlobal(size);
|
||||
try {
|
||||
if(!QueryWorkingSet(process,buffer,size)) throw new System.ComponentModel.Win32Exception();
|
||||
long count=Marshal.ReadIntPtr(buffer).ToInt64(), pages=0;
|
||||
if(count>(size/IntPtr.Size)-1) throw new InvalidOperationException("Working set buffer is too small");
|
||||
for(int i=0;i<count;i++) if((Marshal.ReadIntPtr(buffer,(i+1)*IntPtr.Size).ToInt64() & 256)==0) pages++;
|
||||
return pages*Environment.SystemPageSize;
|
||||
} finally { Marshal.FreeHGlobal(buffer); }
|
||||
}
|
||||
}
|
||||
'@
|
||||
$rows=[Collections.Generic.List[object]]::new()
|
||||
foreach($run in 1..$Runs) {
|
||||
foreach($mode in @('baseline','sta','mta','shell','uia','focused','combined')) {
|
||||
$start=[Diagnostics.ProcessStartInfo]::new($exe,$mode)
|
||||
$start.UseShellExecute=$false; $start.CreateNoWindow=$true
|
||||
$start.RedirectStandardOutput=$true; $start.RedirectStandardInput=$true; $start.RedirectStandardError=$true
|
||||
$process=[Diagnostics.Process]::Start($start)
|
||||
$errors=$process.StandardError.ReadToEndAsync()
|
||||
try {
|
||||
foreach($expected in @('baseline','com','active','released','uninitialized')) {
|
||||
$pending=$process.StandardOutput.ReadLineAsync()
|
||||
if(!$pending.Wait(15000)) { throw "Timeout: $mode/$expected" }
|
||||
$phase=$pending.Result
|
||||
if($phase -ne $expected) { throw "Unexpected phase '$phase', expected $expected" }
|
||||
Start-Sleep -Milliseconds $SettleMilliseconds
|
||||
$process.Refresh()
|
||||
$rows.Add([pscustomobject]@{Run=$run;Mode=$mode;Phase=$phase;PrivateBytes=$process.PrivateMemorySize64;PrivateWorkingSet=[ComponentWorkingSet]::PrivateBytes($process.Handle);WorkingSet=$process.WorkingSet64;Threads=$process.Threads.Count;Modules=@($process.Modules | ForEach-Object ModuleName)})
|
||||
$process.StandardInput.Write("`n"); $process.StandardInput.Flush()
|
||||
}
|
||||
if(!$process.WaitForExit(15000)) { throw 'Exit timeout' }
|
||||
if($process.ExitCode) { throw "Probe failed: $($errors.Result)" }
|
||||
$errors.Result | Set-Content (Join-Path $output "$run-$mode.txt")
|
||||
} finally {
|
||||
if(!$process.HasExited) { $process.Kill(); $process.WaitForExit() }
|
||||
$process.Dispose()
|
||||
$rows | ConvertTo-Json -Depth 5 | Set-Content (Join-Path $output 'results.json')
|
||||
}
|
||||
Write-Host "Completed $run/$Runs $mode"
|
||||
}
|
||||
}
|
||||
$rows | Group-Object Mode,Phase | ForEach-Object {
|
||||
[pscustomobject]@{Case=$_.Name;PrivateMiB=[math]::Round(($_.Group.PrivateBytes | Measure-Object -Average).Average/1MB,3);PrivateWorkingSetMiB=[math]::Round(($_.Group.PrivateWorkingSet | Measure-Object -Average).Average/1MB,3)}
|
||||
} | Format-Table
|
||||
@@ -0,0 +1,65 @@
|
||||
// Standalone attribution probe. No clipboard, registry or user input mutations.
|
||||
#include <windows.h>
|
||||
#include <shlobj.h>
|
||||
#include <propkey.h>
|
||||
#include <propvarutil.h>
|
||||
#include <uiautomation.h>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <cstdlib>
|
||||
|
||||
static void check(HRESULT hr) {
|
||||
if (FAILED(hr)) { fprintf(stderr,"HRESULT=%08lx\n",(unsigned long)hr); exit(2); }
|
||||
}
|
||||
static void phase(const char *name) {
|
||||
printf("%s\n",name); fflush(stdout);
|
||||
if (getchar()==EOF) exit(3);
|
||||
}
|
||||
int main(int argc,char **argv) {
|
||||
const char *mode=argc>1?argv[1]:"baseline";
|
||||
const bool baseline=!strcmp(mode,"baseline");
|
||||
const bool shell=!strcmp(mode,"shell") || !strcmp(mode,"combined");
|
||||
const bool uia=!strcmp(mode,"uia") || !strcmp(mode,"focused") || !strcmp(mode,"combined");
|
||||
phase("baseline");
|
||||
if(!baseline) check(CoInitializeEx(nullptr,!strcmp(mode,"sta") || shell ? COINIT_APARTMENTTHREADED : COINIT_MULTITHREADED));
|
||||
phase("com");
|
||||
IShellLinkW *link{}; IPropertyStore *store{}; IPersistFile *file{};
|
||||
IUIAutomation2 *automation{}; IUIAutomationElement *element{}; IUIAutomationTextPattern *pattern{};
|
||||
wchar_t temporary[MAX_PATH]{},shortcut[MAX_PATH]{},exe[MAX_PATH]{};
|
||||
if(shell) {
|
||||
check(CoCreateInstance(CLSID_ShellLink,nullptr,CLSCTX_INPROC_SERVER,IID_PPV_ARGS(&link)));
|
||||
GetModuleFileNameW(nullptr,exe,MAX_PATH);
|
||||
check(link->SetPath(exe)); check(link->SetIconLocation(exe,0));
|
||||
check(link->QueryInterface(IID_PPV_ARGS(&store)));
|
||||
PROPVARIANT value{}; check(InitPropVariantFromString(L"KSwitcher.MemoryProbe",&value));
|
||||
check(store->SetValue(PKEY_AppUserModel_ID,value)); PropVariantClear(&value);
|
||||
GUID id{0xe3f78047,0xf0ad,0x49b6,{0x90,0x74,0x41,0x19,0x31,0x70,0x61,0x67}};
|
||||
value.vt=VT_CLSID; value.puuid=&id;
|
||||
check(store->SetValue(PKEY_AppUserModel_ToastActivatorCLSID,value)); check(store->Commit());
|
||||
PWSTR programs{}; check(SHGetKnownFolderPath(FOLDERID_Programs,0,nullptr,&programs)); CoTaskMemFree(programs);
|
||||
GetTempPathW(MAX_PATH,temporary); if(!GetTempFileNameW(temporary,L"ksm",0,shortcut)) return 4;
|
||||
check(link->QueryInterface(IID_PPV_ARGS(&file))); check(file->Save(shortcut,TRUE));
|
||||
}
|
||||
if(uia) {
|
||||
check(CoCreateInstance(CLSID_CUIAutomation8,nullptr,CLSCTX_INPROC_SERVER,IID_PPV_ARGS(&automation)));
|
||||
check(automation->put_ConnectionTimeout(200)); check(automation->put_TransactionTimeout(200));
|
||||
if(strcmp(mode,"uia")) {
|
||||
HRESULT focused=automation->GetFocusedElement(&element);
|
||||
fprintf(stderr,"focus=%08lx\n",(unsigned long)focused);
|
||||
if(SUCCEEDED(focused) && element) {
|
||||
BOOL password=TRUE,enabled=FALSE; CONTROLTYPEID type{};
|
||||
element->get_CurrentIsPassword(&password); element->get_CurrentIsEnabled(&enabled); element->get_CurrentControlType(&type);
|
||||
fprintf(stderr,"type=%d enabled=%d protected=%d\n",type,enabled,password);
|
||||
if(!password) fprintf(stderr,"pattern=%08lx\n",(unsigned long)element->GetCurrentPatternAs(UIA_TextPatternId,IID_PPV_ARGS(&pattern)));
|
||||
}
|
||||
}
|
||||
}
|
||||
phase("active");
|
||||
if(pattern) pattern->Release(); if(element) element->Release(); if(automation) automation->Release();
|
||||
if(file) file->Release(); if(store) store->Release(); if(link) link->Release();
|
||||
if(shortcut[0]) DeleteFileW(shortcut);
|
||||
phase("released");
|
||||
if(!baseline) { CoFreeUnusedLibrariesEx(0,0); CoUninitialize(); }
|
||||
phase("uninitialized");
|
||||
return 0;
|
||||
}
|
||||
Reference in New Issue
Block a user