Fix clipboard history support in v0.20

This commit is contained in:
f1den committed 2026-08-10 17:37:26 +03:00
1 parent ceb0b5552e
commit 784be4eb67
8 files changed
+278 -25

No files matched your search

+19 -15
View File
@@ -48,65 +48,69 @@ echo Compiler: !CXX_PATH!
echo Target: !TARGET!
echo.
echo [1/13] Compiling capture.cpp
echo [1/14] Compiling capture.cpp
"!CXX_PATH!" !COMMON! -c src\capture.cpp -o build\capture.o
if errorlevel 1 goto :build_failed
echo [2/13] Compiling image_io.cpp
echo [2/14] Compiling image_io.cpp
"!CXX_PATH!" !COMMON! -c src\image_io.cpp -o build\image_io.o
if errorlevel 1 goto :build_failed
echo [3/13] Compiling gif_recorder.cpp
echo [3/14] Compiling clipboard_history.cpp
"!CXX_PATH!" !COMMON! -c src\clipboard_history.cpp -o build\clipboard_history.o
if errorlevel 1 goto :build_failed
echo [4/14] Compiling gif_recorder.cpp
"!CXX_PATH!" !COMMON! -c src\gif_recorder.cpp -o build\gif_recorder.o
if errorlevel 1 goto :build_failed
echo [4/13] Compiling gif_options_window.cpp
echo [5/14] Compiling gif_options_window.cpp
"!CXX_PATH!" !COMMON! -c src\gif_options_window.cpp -o build\gif_options_window.o
if errorlevel 1 goto :build_failed
echo [5/13] Compiling overlay.cpp
echo [6/14] Compiling overlay.cpp
"!CXX_PATH!" !COMMON! -c src\overlay.cpp -o build\overlay.o
if errorlevel 1 goto :build_failed
echo [6/13] Compiling notifications.cpp
echo [7/14] Compiling notifications.cpp
"!CXX_PATH!" !COMMON! -c src\notifications.cpp -o build\notifications.o
if errorlevel 1 goto :build_failed
echo [7/13] Compiling settings.cpp
echo [8/14] Compiling settings.cpp
"!CXX_PATH!" !COMMON! -c src\settings.cpp -o build\settings.o
if errorlevel 1 goto :build_failed
echo [8/13] Compiling startup.cpp
echo [9/14] Compiling startup.cpp
"!CXX_PATH!" !COMMON! -c src\startup.cpp -o build\startup.o
if errorlevel 1 goto :build_failed
echo [9/13] Compiling settings_window.cpp
echo [10/14] Compiling settings_window.cpp
"!CXX_PATH!" !COMMON! -c src\settings_window.cpp -o build\settings_window.o
if errorlevel 1 goto :build_failed
echo [10/13] Compiling uploader.cpp
echo [11/14] Compiling uploader.cpp
"!CXX_PATH!" !COMMON! -c src\uploader.cpp -o build\uploader.o
if errorlevel 1 goto :build_failed
echo [11/13] Compiling main.cpp
echo [12/14] Compiling main.cpp
"!CXX_PATH!" !COMMON! -c src\main.cpp -o build\main.o
if errorlevel 1 goto :build_failed
if defined WINDRES_PATH (
echo [12/13] Compiling resources
echo [13/14] Compiling resources
"!WINDRES_PATH!" -Iinclude -Ires res\app.rc -O coff -o build\app_res.o
if errorlevel 1 goto :build_failed
set "RES_OBJECT=build\app_res.o"
) else (
echo [12/13] Resources skipped
echo [13/14] Resources skipped
echo [WARN] Win64 windres was not found. The EXE will still build, but without the embedded icon/version/manifest.
echo [WARN] Install the full MSYS2 mingw-w64-x86_64-binutils package to restore resources.
)
echo [13/13] Linking Win64 executable
echo [14/14] Linking Win64 executable
"!CXX_PATH!" -municode -mwindows -static -static-libgcc -static-libstdc++ ^
-Wl,--gc-sections -Wl,--nxcompat -Wl,--dynamicbase -Wl,--high-entropy-va ^
build\capture.o build\image_io.o build\gif_recorder.o build\gif_options_window.o ^
build\capture.o build\image_io.o build\clipboard_history.o build\gif_recorder.o build\gif_options_window.o ^
build\overlay.o build\notifications.o ^
build\settings.o build\startup.o build\settings_window.o build\uploader.o ^
build\main.o !RES_OBJECT! ^
+14
View File
@@ -0,0 +1,14 @@
#pragma once
#include "config.hpp"
#include <windows.h>
#include <vector>
namespace clipboard_history {
// Publishes an encoded bitmap stream through Windows.ApplicationModel.DataTransfer
// so Windows 10/11 clipboard history can ingest it. This intentionally happens
// before the richer Win32 clipboard payload is installed.
[[nodiscard]] bool publishBitmap(const std::vector<BYTE>& encodedBitmap,
DWORD settleMilliseconds = 450);
} // namespace clipboard_history
+1
View File
@@ -12,6 +12,7 @@
[[nodiscard]] bool saveBitmapAsJpeg(Gdiplus::Bitmap& bitmap, const std::wstring& path, int quality);
[[nodiscard]] bool copyBitmapToClipboard(HWND owner, Gdiplus::Bitmap& bitmap);
[[nodiscard]] bool encodeBitmapAsPng(Gdiplus::Bitmap& bitmap, std::vector<BYTE>& output);
[[nodiscard]] bool encodeBitmapForClipboardHistory(Gdiplus::Bitmap& bitmap, std::vector<BYTE>& output);
// Reserves a non-existing incremented file name with CREATE_NEW. The returned
// empty file belongs to the caller and may safely be filled/overwritten.
+4 -4
View File
@@ -3,8 +3,8 @@ IDI_APP_ICON ICON "nightshot.ico"
IDR_APP_MANIFEST RT_MANIFEST "app.manifest"
1 VERSIONINFO
FILEVERSION 0,19,0,0
PRODUCTVERSION 0,19,0,0
FILEVERSION 0,20,0,0
PRODUCTVERSION 0,20,0,0
FILEFLAGSMASK 0x3fL
FILEFLAGS 0x0L
FILEOS 0x40004L
@@ -17,11 +17,11 @@ BEGIN
BEGIN
VALUE "CompanyName", "NightSync"
VALUE "FileDescription", "NightShot Win64 screenshot utility"
VALUE "FileVersion", "0.19.0"
VALUE "FileVersion", "0.20.0"
VALUE "InternalName", "NightShot"
VALUE "OriginalFilename", "NightShot.exe"
VALUE "ProductName", "NightShot"
VALUE "ProductVersion", "0.19.0"
VALUE "ProductVersion", "0.20.0"
END
END
BLOCK "VarFileInfo"
+174
View File
@@ -0,0 +1,174 @@
#include "clipboard_history.hpp"
#include <objidl.h>
#include <roapi.h>
#include <shcore.h>
#include <windows.applicationmodel.datatransfer.h>
#include <windows.storage.streams.h>
#include <wrl/client.h>
#include <wrl/wrappers/corewrappers.h>
#include <cstring>
namespace clipboard_history {
namespace {
using Microsoft::WRL::ComPtr;
using Microsoft::WRL::Wrappers::HStringReference;
using ABI::Windows::ApplicationModel::DataTransfer::IClipboardStatics;
using ABI::Windows::ApplicationModel::DataTransfer::IClipboardStatics2;
using ABI::Windows::ApplicationModel::DataTransfer::IClipboardContentOptions;
using ABI::Windows::ApplicationModel::DataTransfer::IDataPackage;
using ABI::Windows::Storage::Streams::IRandomAccessStream;
using ABI::Windows::Storage::Streams::IRandomAccessStreamReference;
using ABI::Windows::Storage::Streams::IRandomAccessStreamReferenceStatics;
struct RuntimeScope {
bool uninitialize = false;
RuntimeScope() {
const HRESULT result = RoInitialize(RO_INIT_SINGLETHREADED);
if (result == S_OK || result == S_FALSE) {
uninitialize = true;
} else if (result == RPC_E_CHANGED_MODE) {
// The caller already initialized COM/WinRT using another apartment.
// Static WinRT clipboard APIs remain usable on that thread.
} else {
initializationResult = result;
}
}
~RuntimeScope() {
if (uninitialize) RoUninitialize();
}
HRESULT initializationResult = S_OK;
[[nodiscard]] bool ready() const {
return SUCCEEDED(initializationResult);
}
};
HRESULT makeRandomAccessReference(const std::vector<BYTE>& bytes,
ComPtr<IRandomAccessStreamReference>& reference) {
if (bytes.empty()) return E_INVALIDARG;
HGLOBAL memory = GlobalAlloc(GMEM_MOVEABLE, bytes.size());
if (!memory) return E_OUTOFMEMORY;
void* destination = GlobalLock(memory);
if (!destination) {
const HRESULT error = HRESULT_FROM_WIN32(GetLastError());
GlobalFree(memory);
return error;
}
std::memcpy(destination, bytes.data(), bytes.size());
GlobalUnlock(memory);
ComPtr<IStream> stream;
HRESULT result = CreateStreamOnHGlobal(memory, TRUE, stream.ReleaseAndGetAddressOf());
if (FAILED(result)) {
GlobalFree(memory);
return result;
}
LARGE_INTEGER zero{};
result = stream->Seek(zero, STREAM_SEEK_SET, nullptr);
if (FAILED(result)) return result;
ComPtr<IRandomAccessStream> randomAccessStream;
result = CreateRandomAccessStreamOverStream(
stream.Get(), BSOS_DEFAULT,
IID_PPV_ARGS(randomAccessStream.ReleaseAndGetAddressOf()));
if (FAILED(result)) return result;
ComPtr<IRandomAccessStreamReferenceStatics> referenceFactory;
result = RoGetActivationFactory(
HStringReference(RuntimeClass_Windows_Storage_Streams_RandomAccessStreamReference).Get(),
IID_PPV_ARGS(referenceFactory.ReleaseAndGetAddressOf()));
if (FAILED(result)) return result;
return referenceFactory->CreateFromStream(
randomAccessStream.Get(), reference.ReleaseAndGetAddressOf());
}
} // namespace
bool publishBitmap(const std::vector<BYTE>& encodedBitmap, DWORD settleMilliseconds) {
if (encodedBitmap.empty()) return false;
RuntimeScope runtime;
if (!runtime.ready()) return false;
ComPtr<IRandomAccessStreamReference> streamReference;
HRESULT result = makeRandomAccessReference(encodedBitmap, streamReference);
if (FAILED(result)) return false;
ComPtr<IInspectable> inspectablePackage;
result = RoActivateInstance(
HStringReference(RuntimeClass_Windows_ApplicationModel_DataTransfer_DataPackage).Get(),
inspectablePackage.ReleaseAndGetAddressOf());
if (FAILED(result)) return false;
ComPtr<IDataPackage> package;
result = inspectablePackage.As(&package);
if (FAILED(result)) return false;
result = package->SetBitmap(streamReference.Get());
if (FAILED(result)) return false;
ComPtr<IClipboardStatics> clipboard;
result = RoGetActivationFactory(
HStringReference(RuntimeClass_Windows_ApplicationModel_DataTransfer_Clipboard).Get(),
IID_PPV_ARGS(clipboard.ReleaseAndGetAddressOf()));
if (FAILED(result)) return false;
bool contentAccepted = false;
// Windows 10 1809+ exposes SetContentWithOptions. Unlike SetContent, this
// API can be used when NightShot is no longer the foreground window (most
// importantly after an asynchronous GIF encode), and lets us explicitly
// mark the compact bitmap representation as history-eligible.
ComPtr<IClipboardStatics2> clipboard2;
result = RoGetActivationFactory(
HStringReference(RuntimeClass_Windows_ApplicationModel_DataTransfer_Clipboard).Get(),
IID_PPV_ARGS(clipboard2.ReleaseAndGetAddressOf()));
if (SUCCEEDED(result) && clipboard2) {
ComPtr<IInspectable> inspectableOptions;
result = RoActivateInstance(
HStringReference(RuntimeClass_Windows_ApplicationModel_DataTransfer_ClipboardContentOptions).Get(),
inspectableOptions.ReleaseAndGetAddressOf());
if (SUCCEEDED(result) && inspectableOptions) {
ComPtr<IClipboardContentOptions> options;
result = inspectableOptions.As(&options);
if (SUCCEEDED(result) && options) {
result = options->put_IsAllowedInHistory(true);
if (SUCCEEDED(result)) {
boolean accepted = 0;
result = clipboard2->SetContentWithOptions(
package.Get(), options.Get(), &accepted);
contentAccepted = SUCCEEDED(result) && accepted != 0;
}
}
}
}
// Compatibility fallback for Windows versions that do not expose the
// options API. SetContent requires the application to be foreground, so
// this path is intentionally second choice.
if (!contentAccepted) {
result = clipboard->SetContent(package.Get());
if (FAILED(result)) return false;
contentAccepted = true;
}
// Materialize the DataPackage now, while all stream objects are alive.
result = clipboard->Flush();
if (FAILED(result)) return false;
// Clipboard History consumes clipboard updates asynchronously. Keep this
// history-friendly item current long enough for cbdhsvc to observe it before
// NightShot replaces it with the richer Win32 PNG/DIB/GIF payload.
if (settleMilliseconds != 0) Sleep(settleMilliseconds);
return true;
}
} // namespace clipboard_history
+17 -4
View File
@@ -1,6 +1,7 @@
#include "gif_recorder.hpp"
#include "app_messages.hpp"
#include "clipboard_history.hpp"
#include "image_io.hpp"
#include "settings.hpp"
@@ -262,13 +263,25 @@ bool copyGifToClipboard(const Session& session) {
const std::vector<BYTE> gif = readAllBytes(session.path);
if (gif.empty()) return false;
std::vector<BYTE> historyPreview;
bool historyPublished = false;
if (!session.firstFrame.empty() && session.width > 0 && session.height > 0 &&
session.stride >= session.width * 4) {
Gdiplus::Bitmap firstFrame(session.width, session.height, session.stride,
PixelFormat32bppARGB,
const_cast<BYTE*>(session.firstFrame.data()));
historyPublished = firstFrame.GetLastStatus() == Gdiplus::Ok &&
encodeBitmapForClipboardHistory(firstFrame, historyPreview) &&
clipboard_history::publishBitmap(historyPreview);
}
HGLOBAL gifGeneric = globalBytes(gif.data(), gif.size());
HGLOBAL gifMime = globalBytes(gif.data(), gif.size());
HGLOBAL files = dropFiles(session.path);
HGLOBAL dibV5 = firstFrameDibV5(session);
HGLOBAL dib = firstFrameDib24(session);
HBITMAP bitmap = firstFrameBitmap(session);
HGLOBAL includeInHistory = serializedDword(1);
HGLOBAL includeInHistory = serializedDword(historyPublished ? 0u : 1u);
if (!gifGeneric && !gifMime && !files && !dibV5 && !dib && !bitmap) {
if (includeInHistory) GlobalFree(includeInHistory);
return false;
@@ -322,9 +335,9 @@ bool copyGifToClipboard(const Session& session) {
transferred = true;
}
// Explicitly ask Windows 10/11 clipboard monitoring to retain this item in
// Win+V. The first frame is also published as standard bitmap formats so
// history has a format it can display even when it ignores the custom GIF.
// The WinRT history proxy above carries the first frame. Keep the rich
// animated GIF replacement out of history when that succeeded; otherwise
// fall back to the documented Win32 history opt-in.
const UINT historyFormat = RegisterClipboardFormatW(L"CanIncludeInClipboardHistory");
if (includeInHistory && historyFormat && SetClipboardData(historyFormat, includeInHistory)) {
includeInHistory = nullptr;
+48 -1
View File
@@ -1,4 +1,5 @@
#include "image_io.hpp"
#include "clipboard_history.hpp"
#include <commdlg.h>
#include <shlobj.h>
#include <filesystem>
@@ -223,6 +224,39 @@ bool openClipboardWithRetry(HWND owner) {
return false;
}
bool makeClipboardHistoryPreview(Gdiplus::Bitmap& source, std::vector<BYTE>& output) {
output.clear();
const UINT sourceWidth = source.GetWidth();
const UINT sourceHeight = source.GetHeight();
if (sourceWidth == 0 || sourceHeight == 0) return false;
// Win+V has a small per-item budget. Keep the history representation at
// most 1280x720; the actual clipboard remains full resolution below.
constexpr UINT kMaximumWidth = 1280;
constexpr UINT kMaximumHeight = 720;
const double scale = std::min(
1.0, std::min(static_cast<double>(kMaximumWidth) / sourceWidth,
static_cast<double>(kMaximumHeight) / sourceHeight));
if (scale >= 0.999) return encodeBitmapAsPng(source, output);
const UINT width = std::max<UINT>(1, static_cast<UINT>(sourceWidth * scale + 0.5));
const UINT height = std::max<UINT>(1, static_cast<UINT>(sourceHeight * scale + 0.5));
Gdiplus::Bitmap preview(static_cast<INT>(width), static_cast<INT>(height),
PixelFormat32bppARGB);
if (preview.GetLastStatus() != Gdiplus::Ok) return false;
Gdiplus::Graphics graphics(&preview);
graphics.SetCompositingMode(Gdiplus::CompositingModeSourceCopy);
graphics.Clear(Gdiplus::Color(255, 255, 255, 255));
graphics.SetCompositingMode(Gdiplus::CompositingModeSourceOver);
graphics.SetInterpolationMode(Gdiplus::InterpolationModeHighQualityBicubic);
graphics.SetPixelOffsetMode(Gdiplus::PixelOffsetModeHighQuality);
if (graphics.DrawImage(&source, 0, 0, static_cast<INT>(width),
static_cast<INT>(height)) != Gdiplus::Ok) return false;
graphics.Flush(Gdiplus::FlushIntentionSync);
return encodeBitmapAsPng(preview, output);
}
HGLOBAL serializedDword(DWORD value) {
HGLOBAL memory = GlobalAlloc(GMEM_MOVEABLE, sizeof(DWORD));
if (!memory) return nullptr;
@@ -237,15 +271,28 @@ HGLOBAL serializedDword(DWORD value) {
}
} // namespace
bool encodeBitmapForClipboardHistory(Gdiplus::Bitmap& bitmap, std::vector<BYTE>& output) {
return makeClipboardHistoryPreview(bitmap, output);
}
bool copyBitmapToClipboard(HWND owner, Gdiplus::Bitmap& bitmap) {
OpaqueBgraImage image = renderOpaqueBgra(bitmap);
if (!image.valid()) return false;
std::vector<BYTE> historyPreview;
const bool historyPublished =
encodeBitmapForClipboardHistory(bitmap, historyPreview) &&
clipboard_history::publishBitmap(historyPreview);
HGLOBAL png = makePng(image);
HGLOBAL dibV5 = makeDibV5(image);
HGLOBAL dib = makeDib24(image);
HBITMAP nativeBitmap = makeClipboardBitmap(image);
HGLOBAL includeInHistory = serializedDword(1);
// If the WinRT history item was accepted, explicitly keep the following
// rich Win32 replacement out of history so it cannot create a duplicate or
// displace the compact preview. Fall back to the documented Win32 opt-in
// when WinRT is unavailable.
HGLOBAL includeInHistory = serializedDword(historyPublished ? 0u : 1u);
if (!png && !dibV5 && !dib && !nativeBitmap) {
if (includeInHistory) GlobalFree(includeInHistory);
return false;
+1 -1
View File
@@ -209,7 +209,7 @@ std::unique_ptr<Result> performUpload(const Job& job) {
}
InternetHandle session(WinHttpOpen(
L"NightShot/0.19", WINHTTP_ACCESS_TYPE_AUTOMATIC_PROXY,
L"NightShot/0.20", WINHTTP_ACCESS_TYPE_AUTOMATIC_PROXY,
WINHTTP_NO_PROXY_NAME, WINHTTP_NO_PROXY_BYPASS, 0));
if (!session.value) {
result->message = L"WinHttpOpen: " + win32Error(GetLastError());