Release v0.6.20: edit history images and share smooth wheel handling

This commit is contained in:
2026-10-31 09:12:59 +02:00
parent 6603d28f68
commit 70ea9b10bc
22 changed files with 334 additions and 33 deletions
+70 -11
View File
@@ -1,5 +1,7 @@
#include "../resources/resource.h"
#include "app.h"
#include "history_preview.h"
#include "ui_scroll.h"
#include "configuration.h"
#include "ocr.h"
#include "richtext.h"
@@ -238,6 +240,9 @@ HWND control(HWND parent, const wchar_t *cls, const wchar_t *title, DWORD style,
scale(parent, width), scale(parent, height), parent, (HMENU)(INT_PTR)id,
GetModuleHandleW(nullptr), nullptr);
SendMessageW(w, WM_SETFONT, (WPARAM)GetPropW(parent, L"Font"), TRUE);
if(wcscmp(cls,L"LISTBOX")==0 || wcscmp(cls,WC_LISTVIEWW)==0 ||
wcscmp(cls,L"COMBOBOX")==0 || (wcscmp(cls,L"EDIT")==0 && (style&ES_MULTILINE)))
enableWheelBatching(w,wcscmp(cls,L"LISTBOX")==0);
if (auto *layout = windowLayoutState(parent); layout && layout->scrolling)
{
layout->controls.push_back({w, {x, y, x + width, y + height}});
@@ -368,11 +373,12 @@ HWND makeWindow(App &app, const wchar_t *cls, const wchar_t *title, WNDPROC proc
int top = monitor.rcWork.top + std::max(0L, (monitor.rcWork.bottom - monitor.rcWork.top - outerHeight) / 2);
SetPropW(w, L"WindowLayout", new WindowLayout{width, height, 0, 0, !(extra & WS_THICKFRAME)});
SetWindowSubclass(w, layoutProc, 2, 0);
if(windowLayoutState(w)->scrolling) enableWheelBatching(w);
RECT initial = fitWindowRect({left, top, left + outerWidth, top + outerHeight}, monitor.rcWork);
SetWindowPos(w, nullptr, initial.left, initial.top, initial.right - initial.left, initial.bottom - initial.top,
SWP_NOACTIVATE | SWP_NOZORDER);
arrangeWindow(w);
SetWindowTextW(w, title);
SetWindowTextW(w, localBuildTitle(title).c_str());
auto smallIcon = (HICON)LoadImageW(app.instance, MAKEINTRESOURCEW(IDI_KSWITCHER), IMAGE_ICON,
GetSystemMetricsForDpi(SM_CXSMICON, dpi),
GetSystemMetricsForDpi(SM_CYSMICON, dpi), LR_SHARED);
@@ -478,7 +484,17 @@ struct HistoryView
std::map<std::wstring, std::string> richTexts;
HWND rich{};
std::set<std::wstring> pending;
bool scrolling{};
};
bool selectedImage(HWND w, HistoryView *v)
{
int i=(int)SendDlgItemMessageW(w,102,LB_GETCURSEL,0,0);
return i>=0 && i<(int)v->items.size() && v->items[i].image;
}
void updateHistoryActions(HWND w, HistoryView *v)
{
SetDlgItemTextW(w,114,selectedImage(w,v)?L"Edit image":L"Open item");
}
std::wstring selected(HWND w, HistoryView *v)
{
int i = (int)SendDlgItemMessageW(w, 102, LB_GETCURSEL, 0, 0);
@@ -507,6 +523,7 @@ void search(HWND w, HistoryView *v)
}
void requestPreview(HWND w, HistoryView *v, const Entry &entry)
{
if(v->scrolling) return;
const auto id = entry.id;
const bool image = entry.image;
if (v->thumbnails.contains(id) || v->richTexts.contains(id) || !v->pending.insert(id).second)
@@ -690,6 +707,12 @@ LRESULT CALLBACK historyProc(HWND w, UINT msg, WPARAM wp, LPARAM lp)
{
switch (msg)
{
case ScrollActivity:
if((HWND)wp==GetDlgItem(w,102)) {
v->scrolling=lp!=0;
if(!v->scrolling) InvalidateRect((HWND)wp,nullptr,FALSE);
}
return 0;
case WM_COMMAND:
{
int id = LOWORD(wp);
@@ -702,6 +725,7 @@ LRESULT CALLBACK historyProc(HWND w, UINT msg, WPARAM wp, LPARAM lp)
}
if (id == 102 && HIWORD(wp) == LBN_SELCHANGE)
{
updateHistoryActions(w,v);
return 0;
}
if (id == 102 && HIWORD(wp) == LBN_DBLCLK)
@@ -747,10 +771,13 @@ LRESULT CALLBACK historyProc(HWND w, UINT msg, WPARAM wp, LPARAM lp)
if (result->generation != v->generation)
return 0;
auto old = selected(w, v);
int top=(int)SendDlgItemMessageW(w,102,LB_GETTOPINDEX,0,0);
auto topId=top>=0 && top<(int)v->items.size()?v->items[top].id:L"";
v->items = std::move(result->items);
SendDlgItemMessageW(w, 102, WM_SETREDRAW, FALSE, 0);
SendDlgItemMessageW(w, 102, LB_RESETCONTENT, 0, 0);
int select = 0;
int restoreTop=0;
for (size_t i = 0; i < v->items.size(); ++i)
{
auto &e = v->items[i];
@@ -758,8 +785,11 @@ LRESULT CALLBACK historyProc(HWND w, UINT msg, WPARAM wp, LPARAM lp)
SendDlgItemMessageW(w, 102, LB_ADDSTRING, 0, (LPARAM)text.c_str());
if (e.id == old)
select = (int)i;
if(e.id==topId) restoreTop=(int)i;
}
SendDlgItemMessageW(w, 102, LB_SETCURSEL, select, 0);
SendDlgItemMessageW(w,102,LB_SETTOPINDEX,restoreTop,0);
updateHistoryActions(w,v);
SendDlgItemMessageW(w, 102, WM_SETREDRAW, TRUE, 0);
InvalidateRect(GetDlgItem(w, 102), nullptr, TRUE);
return 0;
@@ -770,16 +800,25 @@ LRESULT CALLBACK historyProc(HWND w, UINT msg, WPARAM wp, LPARAM lp)
if (result->generation != v->generation || !IsWindowVisible(w))
return 0;
v->pending.erase(result->id);
// At most ~3.3 MiB, irrespective of history length or source image size.
if (v->thumbnails.size() >= 32)
v->thumbnails.erase(v->thumbnails.begin());
if (v->richTexts.size() >= 32)
v->richTexts.erase(v->richTexts.begin());
auto list=GetDlgItem(w,102);
RECT client{}; GetClientRect(list,&client);
std::set<std::wstring> visible;
RECT changed{}; bool repaint=false;
int top=(int)SendMessageW(list,LB_GETTOPINDEX,0,0);
for(int i=std::max(0,top);i<(int)v->items.size();++i) {
RECT row{}, intersection{};
if(SendMessageW(list,LB_GETITEMRECT,i,(LPARAM)&row)==LB_ERR || row.top>=client.bottom) break;
if(!IntersectRect(&intersection,&row,&client)) continue;
visible.insert(v->items[i].id);
if(v->items[i].id==result->id) { changed=intersection; repaint=true; }
}
if (!result->rtf.empty())
v->richTexts[result->id] = std::move(result->rtf);
else
v->thumbnails[result->id] = std::move(result->pixels);
InvalidateRect(GetDlgItem(w, 102), nullptr, FALSE);
trimHistoryPreviews(v->thumbnails,visible);
trimHistoryPreviews(v->richTexts,visible);
if(repaint) InvalidateRect(list,&changed,FALSE);
return 0;
}
case WM_MEASUREITEM:
@@ -798,15 +837,29 @@ LRESULT CALLBACK historyProc(HWND w, UINT msg, WPARAM wp, LPARAM lp)
break;
case WM_CONTEXTMENU:
{
auto list=GetDlgItem(w,102);
if((HWND)wp!=list) break;
POINT p{(short)LOWORD(lp),(short)HIWORD(lp)};
if(lp!=-1) {
POINT hit=p; ScreenToClient(list,&hit);
auto item=SendMessageW(list,LB_ITEMFROMPOINT,0,MAKELPARAM(hit.x,hit.y));
if(HIWORD(item)) return 0;
SendMessageW(list,LB_SETCURSEL,LOWORD(item),0);
updateHistoryActions(w,v);
} else {
RECT row{};
auto index=SendMessageW(list,LB_GETCURSEL,0,0);
if(index==LB_ERR) return 0;
SendMessageW(list,LB_GETITEMRECT,index,(LPARAM)&row);
p={row.left,row.top}; ClientToScreen(list,&p);
}
HMENU menu = CreatePopupMenu();
AppendMenuW(menu, MF_STRING, 110, L"Paste");
AppendMenuW(menu, MF_STRING, 111, L"Paste plain text");
AppendMenuW(menu, MF_STRING, 112, L"Pin / unpin");
AppendMenuW(menu, MF_STRING, 113, L"Delete");
AppendMenuW(menu, MF_STRING, 114, L"Open full item");
AppendMenuW(menu, MF_STRING, 114, selectedImage(w,v)?L"Edit image in screenshot editor":L"Open full item");
AppendMenuW(menu, MF_STRING, 115, L"Copy text from image (OCR)");
POINT p;
GetCursorPos(&p);
int cmd = TrackPopupMenu(menu, TPM_RETURNCMD, p.x, p.y, 0, w, nullptr);
DestroyMenu(menu);
if (cmd)
@@ -1700,11 +1753,17 @@ void showEntry(App &app, const std::wstring &id)
try
{
auto clip = app.history->load(id);
if(std::any_of(clip.formats.begin(),clip.formats.end(),[](const auto &format) {
return format.id==CF_DIB || format.id==CF_DIBV5 || format.name==L"PNG";
})) {
launchScreenshotEditor(app,clip);
return;
}
auto text = clip.text();
if (text.empty())
{
MessageBoxW(app.historyWindow,
L"This item contains an image or files. Use the preview or paste it into the "
L"This item contains files. Paste it into the "
L"destination application.",
L"Clipboard item", MB_OK);
return;