Release v0.6.20: edit history images and share smooth wheel handling
This commit is contained in:
+1
-1
@@ -433,7 +433,7 @@ void App::addTray()
|
||||
icon.uCallbackMessage = TrayMessage;
|
||||
icon.hIcon = (HICON)LoadImageW(instance, MAKEINTRESOURCEW(IDI_KSWITCHER), IMAGE_ICON,
|
||||
GetSystemMetrics(SM_CXSMICON), GetSystemMetrics(SM_CYSMICON), LR_SHARED);
|
||||
wcscpy_s(icon.szTip, L"KSwitcher — keyboard layouts & clipboard");
|
||||
wcscpy_s(icon.szTip, localBuildTitle(L"KSwitcher — keyboard layouts & clipboard").c_str());
|
||||
Shell_NotifyIconW(NIM_ADD, &icon);
|
||||
icon.uVersion = NOTIFYICON_VERSION_4;
|
||||
Shell_NotifyIconW(NIM_SETVERSION, &icon);
|
||||
|
||||
@@ -120,6 +120,7 @@ class App
|
||||
};
|
||||
void showHistory(App &, bool visible = true);
|
||||
void launchScreenshot(App &);
|
||||
void launchScreenshotEditor(App &, const Clip &);
|
||||
void refreshHistory(App &);
|
||||
void showSettings(App &, bool visible = true);
|
||||
void showLogs(App &);
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
#pragma once
|
||||
#include <map>
|
||||
#include <set>
|
||||
#include <string>
|
||||
|
||||
namespace ks {
|
||||
// Keep the visible working set even in unusually tall windows. Never evict a
|
||||
// visible preview to admit another visible preview: that creates a paint loop.
|
||||
template<class T>
|
||||
void trimHistoryPreviews(std::map<std::wstring,T> &cache,
|
||||
const std::set<std::wstring> &visible, size_t limit=32)
|
||||
{
|
||||
for(auto it=cache.begin(); cache.size()>limit && it!=cache.end();)
|
||||
if(!visible.contains(it->first)) it=cache.erase(it);
|
||||
else ++it;
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,16 @@
|
||||
|
||||
namespace ks
|
||||
{
|
||||
inline std::wstring localBuildTitle(const std::wstring &title)
|
||||
{
|
||||
static const bool development=[] {
|
||||
wchar_t path[32768]{};
|
||||
if(!GetModuleFileNameW(nullptr,path,std::size(path))) return false;
|
||||
std::error_code error;
|
||||
return std::filesystem::exists(std::filesystem::path(path).parent_path()/L".local-dev",error);
|
||||
}();
|
||||
return development?title+L" [LOCAL DEV]":title;
|
||||
}
|
||||
using Bytes = std::vector<unsigned char>;
|
||||
struct Format
|
||||
{
|
||||
|
||||
+1
-1
@@ -841,7 +841,7 @@ void edit(HWND w, Capture &c)
|
||||
SetWindowLongPtrW(w,GWL_EXSTYLE,(GetWindowLongPtrW(w,GWL_EXSTYLE)|WS_EX_APPWINDOW)&~WS_EX_TOOLWINDOW);
|
||||
SetWindowLongPtrW(w,GWL_STYLE,WS_OVERLAPPEDWINDOW | WS_CLIPCHILDREN);
|
||||
auto title=L"KSwitcher — Screenshot #"+std::to_wstring(nextEditorIndex++);
|
||||
SetWindowTextW(w,title.c_str());
|
||||
SetWindowTextW(w,localBuildTitle(title).c_str());
|
||||
INITCOMMONCONTROLSEX statusControls{sizeof(statusControls),ICC_BAR_CLASSES};
|
||||
InitCommonControlsEx(&statusControls);
|
||||
CreateWindowExW(0,STATUSCLASSNAMEW,L"",WS_CHILD|WS_VISIBLE|SBARS_TOOLTIPS,
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
#include "app.h"
|
||||
#include "screenshot_notifications.h"
|
||||
|
||||
namespace ks {
|
||||
void launchScreenshot(App &app)
|
||||
static void launch(App &app, const Clip *clip)
|
||||
{
|
||||
auto path=std::filesystem::path(executable()).parent_path()/L"KSwitcher.Screenshot.exe";
|
||||
if (!std::filesystem::exists(path))
|
||||
throw std::runtime_error("KSwitcher.Screenshot.exe is missing. Extract the complete application package.");
|
||||
auto command=L"\""+path.wstring()+L"\" --auto-open-editor="+
|
||||
(app.settings.screenshotAutoOpenEditor?L"1":L"0");
|
||||
auto command=L"\""+path.wstring()+L"\" ";
|
||||
if(clip) command+=L"--open kswitcher-screenshot:"+cacheScreenshotForEditor(*clip);
|
||||
else command+=std::wstring(L"--auto-open-editor=")+(app.settings.screenshotAutoOpenEditor?L"1":L"0");
|
||||
if(app.settings.writeLog) command+=L" --write-log";
|
||||
STARTUPINFOW startup{sizeof(startup)};
|
||||
PROCESS_INFORMATION process{};
|
||||
@@ -20,4 +22,6 @@ void launchScreenshot(App &app)
|
||||
CloseHandle(process.hThread);
|
||||
CloseHandle(process.hProcess);
|
||||
}
|
||||
void launchScreenshot(App &app) { launch(app,nullptr); }
|
||||
void launchScreenshotEditor(App &app,const Clip &clip) { launch(app,&clip); }
|
||||
}
|
||||
|
||||
@@ -112,6 +112,19 @@ std::wstring escapeXml(const std::wstring &s)
|
||||
return result;
|
||||
}
|
||||
}
|
||||
std::wstring cacheScreenshotForEditor(const Clip &clip)
|
||||
{
|
||||
// Normalize PNG/DIBV5 and other supported clipboard images at full resolution.
|
||||
auto pixels=decodeImage(clip);
|
||||
if(pixels.bgra.empty() || pixels.bgra.size()>128*1024*1024-sizeof(BITMAPINFOHEADER))
|
||||
throw std::runtime_error("Image is too large for the screenshot editor");
|
||||
auto dib=imageDib(pixels);
|
||||
auto directory=cacheDirectory(); cleanCache(directory);
|
||||
GUID guid{}; check(CoCreateGuid(&guid)); wchar_t value[40]{}; StringFromGUID2(guid,value,40);
|
||||
std::wstring id(value+1,36);
|
||||
atomicWrite(directory/(id+L".capture"),protect(dib.data));
|
||||
return id;
|
||||
}
|
||||
Clip loadNotifiedScreenshot(const std::wstring &id)
|
||||
{
|
||||
if(!validScreenshotToken(id)) throw std::runtime_error("Invalid screenshot notification");
|
||||
|
||||
@@ -8,4 +8,5 @@ int runScreenshotToastActivator();
|
||||
DWORD startScreenshotActivationListener();
|
||||
bool validScreenshotToken(const std::wstring &);
|
||||
Clip loadNotifiedScreenshot(const std::wstring &);
|
||||
std::wstring cacheScreenshotForEditor(const Clip &);
|
||||
}
|
||||
|
||||
+70
-11
@@ -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;
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
#pragma once
|
||||
#include "platform.h"
|
||||
#include <algorithm>
|
||||
#include <commctrl.h>
|
||||
#include <windowsx.h>
|
||||
|
||||
namespace ks {
|
||||
inline constexpr UINT ScrollActivity=WM_APP+42;
|
||||
inline constexpr UINT_PTR ScrollFrameTimer=0x4b5301, ScrollIdleTimer=0x4b5302;
|
||||
struct WheelBatch {
|
||||
long long vertical{}, horizontal{};
|
||||
WORD keys{};
|
||||
LPARAM point{};
|
||||
bool frame{}, active{}, listBox{};
|
||||
int remainder{};
|
||||
};
|
||||
inline LRESULT CALLBACK wheelBatchProc(HWND w,UINT msg,WPARAM wp,LPARAM lp,UINT_PTR id,DWORD_PTR data)
|
||||
{
|
||||
auto *batch=reinterpret_cast<WheelBatch *>(data);
|
||||
if(msg==WM_MOUSEWHEEL || msg==WM_MOUSEHWHEEL) {
|
||||
(msg==WM_MOUSEWHEEL?batch->vertical:batch->horizontal)+=GET_WHEEL_DELTA_WPARAM(wp);
|
||||
batch->keys=GET_KEYSTATE_WPARAM(wp); batch->point=lp;
|
||||
if(!batch->active) {
|
||||
batch->active=true;
|
||||
SendMessageW(GetParent(w),ScrollActivity,(WPARAM)w,TRUE);
|
||||
}
|
||||
// Do not restart the frame timer: an uninterrupted wheel stream must
|
||||
// keep moving, rather than wait for a debounce interval to expire.
|
||||
if(!batch->frame) batch->frame=SetTimer(w,ScrollFrameTimer,16,nullptr)!=0;
|
||||
SetTimer(w,ScrollIdleTimer,100,nullptr);
|
||||
return 0;
|
||||
}
|
||||
if(msg==WM_TIMER && wp==ScrollFrameTimer) {
|
||||
KillTimer(w,ScrollFrameTimer); batch->frame=false;
|
||||
if(batch->listBox && batch->vertical) {
|
||||
UINT lines=3; SystemParametersInfoW(SPI_GETWHEELSCROLLLINES,0,&lines,0);
|
||||
if(lines==WHEEL_PAGESCROLL) {
|
||||
RECT client{}; GetClientRect(w,&client);
|
||||
auto height=SendMessageW(w,LB_GETITEMHEIGHT,0,0);
|
||||
lines=std::max<LRESULT>(1,client.bottom/std::max<LRESULT>(1,height));
|
||||
}
|
||||
auto delta=batch->vertical+batch->remainder;
|
||||
batch->remainder=(int)(delta%WHEEL_DELTA);
|
||||
auto top=SendMessageW(w,LB_GETTOPINDEX,0,0);
|
||||
auto count=SendMessageW(w,LB_GETCOUNT,0,0);
|
||||
auto next=std::clamp<long long>(top-(delta/WHEEL_DELTA)*lines,0,std::max<LRESULT>(0,count-1));
|
||||
if(next!=top) SendMessageW(w,LB_SETTOPINDEX,(WPARAM)next,0);
|
||||
batch->vertical=0;
|
||||
} else if(batch->vertical) {
|
||||
// Native edit/list-view/window handlers retain their own scrolling
|
||||
// semantics, including system settings and partial wheel deltas.
|
||||
auto delta=(short)std::clamp<long long>(batch->vertical,-32760,32760);
|
||||
batch->vertical-=delta;
|
||||
DefSubclassProc(w,WM_MOUSEWHEEL,MAKEWPARAM(batch->keys,(WORD)delta),batch->point);
|
||||
}
|
||||
if(batch->horizontal) {
|
||||
auto delta=(short)std::clamp<long long>(batch->horizontal,-32760,32760);
|
||||
batch->horizontal-=delta;
|
||||
DefSubclassProc(w,WM_MOUSEHWHEEL,MAKEWPARAM(batch->keys,(WORD)delta),batch->point);
|
||||
}
|
||||
if(batch->vertical || batch->horizontal)
|
||||
batch->frame=SetTimer(w,ScrollFrameTimer,16,nullptr)!=0;
|
||||
return 0;
|
||||
}
|
||||
if(msg==WM_TIMER && wp==ScrollIdleTimer) {
|
||||
if(batch->frame) return 0;
|
||||
KillTimer(w,ScrollIdleTimer); batch->active=false;
|
||||
SendMessageW(GetParent(w),ScrollActivity,(WPARAM)w,FALSE);
|
||||
return 0;
|
||||
}
|
||||
if(msg==WM_NCDESTROY) {
|
||||
KillTimer(w,ScrollFrameTimer); KillTimer(w,ScrollIdleTimer);
|
||||
RemoveWindowSubclass(w,wheelBatchProc,id); delete batch;
|
||||
}
|
||||
return DefSubclassProc(w,msg,wp,lp);
|
||||
}
|
||||
inline void enableWheelBatching(HWND window,bool listBox=false)
|
||||
{
|
||||
if(!window) return;
|
||||
auto *batch=new WheelBatch; batch->listBox=listBox;
|
||||
if(!SetWindowSubclass(window,wheelBatchProc,ScrollFrameTimer,(DWORD_PTR)batch)) delete batch;
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -2,7 +2,7 @@
|
||||
#include "platform.h"
|
||||
namespace ks
|
||||
{
|
||||
inline constexpr wchar_t AppVersion[] = L"0.6.19";
|
||||
inline constexpr wchar_t AppVersion[] = L"0.6.20";
|
||||
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