Release v0.6.23: improve browser selection, Ukrainian correction and PNG saving

This commit is contained in:
2026-11-01 13:39:37 +02:00
parent c0ab02df16
commit 9b231146cb
18 changed files with 219 additions and 24 deletions
+37
View File
@@ -186,6 +186,43 @@ Pixels decodeImage(const Clip &clip, UINT maxWidth, UINT maxHeight)
check(converter->CopyPixels(nullptr, pixels.width * 4, (UINT)pixels.bgra.size(), pixels.bgra.data()));
return pixels;
}
Bytes encodePng(const Pixels &p)
{
if (!p.width || !p.height || uint64_t(p.width) * p.height * 4 != p.bgra.size() ||
p.bgra.size() > UINT_MAX || p.width > UINT_MAX / 4)
throw std::runtime_error("Invalid PNG image dimensions");
Com<IWICImagingFactory> factory;
check(CoCreateInstance(CLSID_WICImagingFactory, nullptr, CLSCTX_INPROC_SERVER,
IID_PPV_ARGS(factory.out())));
Com<IStream> stream;
check(CreateStreamOnHGlobal(nullptr, TRUE, stream.out()));
Com<IWICBitmapEncoder> encoder;
check(factory->CreateEncoder(GUID_ContainerFormatPng, nullptr, encoder.out()));
check(encoder->Initialize(stream.p, WICBitmapEncoderNoCache));
Com<IWICBitmapFrameEncode> frame;
check(encoder->CreateNewFrame(frame.out(), nullptr));
check(frame->Initialize(nullptr));
check(frame->SetSize(p.width, p.height));
auto format = GUID_WICPixelFormat32bppBGRA;
check(frame->SetPixelFormat(&format));
if (format != GUID_WICPixelFormat32bppBGRA)
throw std::runtime_error("Unsupported PNG pixel format");
check(frame->WritePixels(p.height, p.width * 4, (UINT)p.bgra.size(),
const_cast<BYTE *>(p.bgra.data())));
check(frame->Commit());
check(encoder->Commit());
STATSTG stat{};
check(stream->Stat(&stat, STATFLAG_NONAME));
if (stat.cbSize.QuadPart > UINT_MAX)
throw std::runtime_error("PNG image is too large");
check(stream->Seek({}, STREAM_SEEK_SET, nullptr));
Bytes bytes((size_t)stat.cbSize.QuadPart);
ULONG read{};
check(stream->Read(bytes.data(), (ULONG)bytes.size(), &read));
if (read != bytes.size())
throw std::runtime_error("Cannot read encoded PNG image");
return bytes;
}
Format imageDib(const Pixels &p)
{
BITMAPINFOHEADER h{};
+1
View File
@@ -9,6 +9,7 @@ struct Pixels
};
Pixels decodeImage(const Clip &, UINT maxWidth = 0, UINT maxHeight = 0);
Format imageDib(const Pixels &);
Bytes encodePng(const Pixels &);
std::wstring recognizeText(const Clip &);
std::wstring recognizeText(const Clip &, const Settings &);
std::filesystem::path tesseractExecutable(const Settings &);
+3 -2
View File
@@ -396,13 +396,14 @@ bool chord(WORD key, UINT mods)
bool needsSelectionDelay(HWND target)
{
const auto name = lower(processName(target));
return name == L"ms-teams" || name == L"msedgewebview2";
return name == L"ms-teams" || name == L"msedgewebview2" ||
name == L"chrome" || name == L"msedge";
}
bool selectPrevious(size_t count, const std::function<bool()> &valid)
{
if (count > 100000)
return false;
// Teams' WebView editor applies selection changes asynchronously. A burst of
// Chromium and Teams' WebView editors apply selection changes asynchronously. A burst of
// Shift+Left events can all use the same caret position and select only one
// character. Give each change time to settle before sending the next one.
const auto target = GetForegroundWindow();
+78 -2
View File
@@ -28,8 +28,8 @@ bool confirmScreenshotClose(HWND owner)
dialog.hMainIcon=icon;
dialog.dwCommonButtons=TDCBF_YES_BUTTON|TDCBF_NO_BUTTON;
dialog.pszWindowTitle=L"KSwitcher";
dialog.pszMainInstruction=L"Close without copying?";
dialog.pszContent=L"Your latest screenshot changes have not been copied to the clipboard.";
dialog.pszMainInstruction=L"Close without saving or copying?";
dialog.pszContent=L"Your latest screenshot changes have not been saved to a file or copied to the clipboard.";
dialog.nDefaultButton=IDNO;
dialog.lpCallbackData=(LONG_PTR)owner;
dialog.pfCallback=[](HWND window,UINT notification,WPARAM,LPARAM,LONG_PTR data)->HRESULT {
@@ -208,6 +208,7 @@ struct Capture
int hoveredTool=0;
MarkPart hoveredPart=MarkPart::None;
std::wstring statusText;
std::filesystem::path savedPath;
explicit Capture(ScreenshotHost &a) : app(a), autoOpenEditor(a.autoOpenEditor) {}
~Capture() {
if(pencilCursor) DestroyCursor(pencilCursor);
@@ -239,6 +240,7 @@ void updateStatus(Capture &c)
if(c.hoveredTool==30) text=L"Line style: solid, dotted, dashed or dash-dot";
if(c.hoveredTool==31) text=L"Line ends: start, end, both or no arrowheads";
if(c.hoveredTool==35) text=L"Arrow caption: F2 / double-click to edit; drag to reposition";
if(c.hoveredTool==28) text=L"Save (Ctrl+S) | Save As... (Ctrl+Shift+S)";
}
else if(c.inlineText) text=L"Text: Enter = new line | Ctrl+Enter = apply | Esc = cancel";
else if(part==MarkPart::Rotation) text=L"Drag to rotate text | Hold Shift for 15-degree steps";
@@ -337,6 +339,10 @@ void finishInlineText(HWND w,Capture &c,bool commit)
}
LRESULT CALLBACK inlineTextProc(HWND w,UINT msg,WPARAM wp,LPARAM lp,UINT_PTR,DWORD_PTR)
{
if(msg==WM_KEYDOWN && wp=='S' && (GetKeyState(VK_CONTROL)&0x8000)) {
PostMessageW(GetParent(w),WM_COMMAND,(GetKeyState(VK_SHIFT)&0x8000)?29:28,0); return 0;
}
if(msg==WM_CHAR && wp==19) return 0; // Ctrl+S is handled by the editor.
if(msg==WM_KEYDOWN && (wp==VK_ESCAPE || (wp==VK_RETURN && (GetKeyState(VK_CONTROL)&0x8000)))) {
PostMessageW(GetParent(w),FinishTextMessage,wp==VK_ESCAPE?2:1,(LPARAM)w); return 0;
}
@@ -607,6 +613,9 @@ void layout(HWND w, Capture &c)
MoveWindow(GetDlgItem(w,ToolIds[i]),MulDiv(positions[i],dpi,96),MulDiv(6,dpi,96),
MulDiv(32,dpi,96),MulDiv(32,dpi,96),TRUE);
}
SendDlgItemMessageW(w,28,WM_SETFONT,(WPARAM)c.font,TRUE);
MoveWindow(GetDlgItem(w,28),MulDiv(540,dpi,96),MulDiv(6,dpi,96),
MulDiv(78,dpi,96),MulDiv(32,dpi,96),TRUE);
c.panelHeight=c.unit+MulDiv(40,dpi,96);
for(int id:{30,31,32,33,34,35}) SendDlgItemMessageW(w,id,WM_SETFONT,(WPARAM)c.font,TRUE);
MoveWindow(GetDlgItem(w,32),MulDiv(12,dpi,96),c.unit+MulDiv(6,dpi,96),MulDiv(64,dpi,96),MulDiv(24,dpi,96),TRUE);
@@ -821,6 +830,64 @@ void copy(HWND w, Capture &c, DWORD (*publish)(HWND,const Clip &)=writeClipboard
publish(c.app.window, clip);
c.copied=true;
}
void saveToFile(HWND w, Capture &c, const std::filesystem::path &path)
{
finishInlineText(w,c,true);
auto pixels=decodeImage(screenshotClip(c.image->dc,c.image->width,c.image->height,c.marks));
// GDI-rendered screenshot pixels are opaque; GDI does not maintain alpha.
for(size_t i=3;i<pixels.bgra.size();i+=4) pixels.bgra[i]=255;
auto bytes=encodePng(pixels);
GUID id{};
if(FAILED(CoCreateGuid(&id))) throw std::runtime_error("Cannot prepare screenshot output file");
wchar_t unique[40]{}; StringFromGUID2(id,unique,(int)std::size(unique));
auto temporary=path.parent_path()/(L".kswitcher-"+std::wstring(unique)+L".png");
try {
atomicWrite(temporary,bytes);
if(!MoveFileExW(temporary.c_str(),path.c_str(),MOVEFILE_REPLACE_EXISTING|MOVEFILE_WRITE_THROUGH))
throw std::runtime_error("Cannot save screenshot to the selected file");
} catch(...) {
std::error_code ignored; std::filesystem::remove(temporary,ignored); throw;
}
c.savedPath=path;
c.copied=true; // Current edits have been exported, either to file or clipboard.
}
void save(HWND w, Capture &c, bool saveAs=false,
BOOL (WINAPI *chooseFile)(LPOPENFILENAMEW)=GetSaveFileNameW)
{
if(!saveAs && !c.savedPath.empty()) {
saveToFile(w,c,c.savedPath);
return;
}
wchar_t filename[32768]{};
auto proposed=c.savedPath.wstring();
if(proposed.empty()) {
SYSTEMTIME time{}; GetLocalTime(&time);
wchar_t name[100]{};
swprintf_s(name,L"Screenshot-%04u%02u%02u-%02u%02u%02u.png",
time.wYear,time.wMonth,time.wDay,time.wHour,time.wMinute,time.wSecond);
proposed=name;
}
if(proposed.size()>=std::size(filename)) throw std::runtime_error("Screenshot file path is too long");
std::copy(proposed.begin(),proposed.end(),filename);
OPENFILENAMEW dialog{sizeof(dialog)};
dialog.hwndOwner=w;
dialog.lpstrTitle=L"Save screenshot";
dialog.lpstrFilter=L"PNG image (*.png)\0*.png\0\0";
dialog.nFilterIndex=1;
dialog.lpstrFile=filename;
dialog.nMaxFile=(DWORD)std::size(filename);
dialog.lpstrDefExt=L"png";
dialog.Flags=OFN_OVERWRITEPROMPT|OFN_PATHMUSTEXIST|OFN_NOCHANGEDIR|OFN_EXPLORER;
if(!chooseFile(&dialog)) {
if(CommDlgExtendedError()) throw std::runtime_error("Cannot open the screenshot save dialog");
return;
}
if(lower(std::filesystem::path(filename).extension().wstring())!=L".png")
throw std::runtime_error("Choose a filename ending in .png for the screenshot");
saveToFile(w,c,filename);
c.statusText=L"Saved: "+c.savedPath.wstring();
SendDlgItemMessageW(w,50,SB_SETTEXTW,0,(LPARAM)c.statusText.c_str());
}
void edit(HWND w, Capture &c)
{
auto r = region(c.start,c.end);
@@ -849,6 +916,9 @@ void edit(HWND w, Capture &c)
const wchar_t *labels[]{L"Pointer (V): select and edit objects", L"Number (N)", L"Arrow (A); Ctrl+drag between objects to link", L"Straight line (L); Ctrl+drag between objects to link", L"Rectangle (R)", L"Circle (O)", L"Ellipse (E)", L"Freehand line (F); Shift for 45-degree steps; move >50 px off line to turn; Ctrl to draw over objects", L"Highlight (H); Ctrl+drag to paint over an object",
L"Text (T); double-click or F2 to edit", L"Color — selected object and new objects", L"Undo (Ctrl+Z)", L"Redo (Ctrl+Y / Ctrl+Shift+Z)", L"Copy (Ctrl+C)"};
static_assert(std::size(labels)==std::size(ToolIds));
auto saveButton=CreateWindowW(L"BUTTON",L"Save...",WS_CHILD|WS_VISIBLE|WS_TABSTOP|BS_PUSHBUTTON,
0,0,0,0,w,(HMENU)28,c.app.instance,nullptr);
SetWindowSubclass(saveButton,toolHover,1,0);
auto tips=CreateWindowExW(WS_EX_TOPMOST,TOOLTIPS_CLASSW,nullptr,WS_POPUP|TTS_ALWAYSTIP|TTS_NOPREFIX,
CW_USEDEFAULT,CW_USEDEFAULT,CW_USEDEFAULT,CW_USEDEFAULT,w,nullptr,c.app.instance,nullptr);
for (size_t i=0;i<std::size(ToolIds);++i)
@@ -1023,6 +1093,9 @@ LRESULT CALLBACK captureProc(HWND w, UINT msg, WPARAM wp, LPARAM lp)
if(next!=value) { c->remember(c->marks); value=next; resolveScreenshotLinks(c->marks); }
}
if (LOWORD(wp)==13) { copy(w,*c); return 0; }
if (LOWORD(wp)==28 || LOWORD(wp)==29) {
save(w,*c,LOWORD(wp)==29); SetFocus(w); return 0;
}
if (LOWORD(wp)==12 || LOWORD(wp)==18)
{
bool changed=LOWORD(wp)==12?c->history.undo(c->marks):c->history.redo(c->marks);
@@ -1079,6 +1152,9 @@ LRESULT CALLBACK captureProc(HWND w, UINT msg, WPARAM wp, LPARAM lp)
beginMarkText(w,*c,c->selected); return 0;
}
if ((GetKeyState(VK_CONTROL)&0x8000) && wp=='C') { copy(w,*c); return 0; }
if ((GetKeyState(VK_CONTROL)&0x8000) && wp=='S') {
save(w,*c,(GetKeyState(VK_SHIFT)&0x8000)!=0); SetFocus(w); return 0;
}
if ((GetKeyState(VK_CONTROL)&0x8000) && (wp=='Z' || wp=='Y'))
SendMessageW(w,WM_COMMAND,wp=='Y'||(GetKeyState(VK_SHIFT)&0x8000)?18:12,0);
if ((GetKeyState(VK_CONTROL)&0x8000) && wp=='D' && c->selected>=0)
+1 -1
View File
@@ -2,7 +2,7 @@
#include "platform.h"
namespace ks
{
inline constexpr wchar_t AppVersion[] = L"0.6.22";
inline constexpr wchar_t AppVersion[] = L"0.6.23";
inline constexpr UINT UpdateMessage = WM_APP + 7;
inline constexpr UINT UpdateCheckIntervalMs = 6 * 60 * 60 * 1000;
inline constexpr UINT UpdateNotificationRetryMs = 5 * 60 * 1000;