Fix clipboard history support in v0.20
This commit is contained in:
1 parent
ceb0b5552e
commit
784be4eb67
8 files changed
+278
-25
No files matched your search
@@ -48,65 +48,69 @@ echo Compiler: !CXX_PATH!
|
|||||||
echo Target: !TARGET!
|
echo Target: !TARGET!
|
||||||
echo.
|
echo.
|
||||||
|
|
||||||
echo [1/13] Compiling capture.cpp
|
echo [1/14] Compiling capture.cpp
|
||||||
"!CXX_PATH!" !COMMON! -c src\capture.cpp -o build\capture.o
|
"!CXX_PATH!" !COMMON! -c src\capture.cpp -o build\capture.o
|
||||||
if errorlevel 1 goto :build_failed
|
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
|
"!CXX_PATH!" !COMMON! -c src\image_io.cpp -o build\image_io.o
|
||||||
if errorlevel 1 goto :build_failed
|
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
|
"!CXX_PATH!" !COMMON! -c src\gif_recorder.cpp -o build\gif_recorder.o
|
||||||
if errorlevel 1 goto :build_failed
|
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
|
"!CXX_PATH!" !COMMON! -c src\gif_options_window.cpp -o build\gif_options_window.o
|
||||||
if errorlevel 1 goto :build_failed
|
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
|
"!CXX_PATH!" !COMMON! -c src\overlay.cpp -o build\overlay.o
|
||||||
if errorlevel 1 goto :build_failed
|
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
|
"!CXX_PATH!" !COMMON! -c src\notifications.cpp -o build\notifications.o
|
||||||
if errorlevel 1 goto :build_failed
|
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
|
"!CXX_PATH!" !COMMON! -c src\settings.cpp -o build\settings.o
|
||||||
if errorlevel 1 goto :build_failed
|
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
|
"!CXX_PATH!" !COMMON! -c src\startup.cpp -o build\startup.o
|
||||||
if errorlevel 1 goto :build_failed
|
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
|
"!CXX_PATH!" !COMMON! -c src\settings_window.cpp -o build\settings_window.o
|
||||||
if errorlevel 1 goto :build_failed
|
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
|
"!CXX_PATH!" !COMMON! -c src\uploader.cpp -o build\uploader.o
|
||||||
if errorlevel 1 goto :build_failed
|
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
|
"!CXX_PATH!" !COMMON! -c src\main.cpp -o build\main.o
|
||||||
if errorlevel 1 goto :build_failed
|
if errorlevel 1 goto :build_failed
|
||||||
|
|
||||||
if defined WINDRES_PATH (
|
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
|
"!WINDRES_PATH!" -Iinclude -Ires res\app.rc -O coff -o build\app_res.o
|
||||||
if errorlevel 1 goto :build_failed
|
if errorlevel 1 goto :build_failed
|
||||||
set "RES_OBJECT=build\app_res.o"
|
set "RES_OBJECT=build\app_res.o"
|
||||||
) else (
|
) 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] 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 [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++ ^
|
"!CXX_PATH!" -municode -mwindows -static -static-libgcc -static-libstdc++ ^
|
||||||
-Wl,--gc-sections -Wl,--nxcompat -Wl,--dynamicbase -Wl,--high-entropy-va ^
|
-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\overlay.o build\notifications.o ^
|
||||||
build\settings.o build\startup.o build\settings_window.o build\uploader.o ^
|
build\settings.o build\startup.o build\settings_window.o build\uploader.o ^
|
||||||
build\main.o !RES_OBJECT! ^
|
build\main.o !RES_OBJECT! ^
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -12,6 +12,7 @@
|
|||||||
[[nodiscard]] bool saveBitmapAsJpeg(Gdiplus::Bitmap& bitmap, const std::wstring& path, int quality);
|
[[nodiscard]] bool saveBitmapAsJpeg(Gdiplus::Bitmap& bitmap, const std::wstring& path, int quality);
|
||||||
[[nodiscard]] bool copyBitmapToClipboard(HWND owner, Gdiplus::Bitmap& bitmap);
|
[[nodiscard]] bool copyBitmapToClipboard(HWND owner, Gdiplus::Bitmap& bitmap);
|
||||||
[[nodiscard]] bool encodeBitmapAsPng(Gdiplus::Bitmap& bitmap, std::vector<BYTE>& output);
|
[[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
|
// Reserves a non-existing incremented file name with CREATE_NEW. The returned
|
||||||
// empty file belongs to the caller and may safely be filled/overwritten.
|
// empty file belongs to the caller and may safely be filled/overwritten.
|
||||||
|
|||||||
+4
-4
@@ -3,8 +3,8 @@ IDI_APP_ICON ICON "nightshot.ico"
|
|||||||
IDR_APP_MANIFEST RT_MANIFEST "app.manifest"
|
IDR_APP_MANIFEST RT_MANIFEST "app.manifest"
|
||||||
|
|
||||||
1 VERSIONINFO
|
1 VERSIONINFO
|
||||||
FILEVERSION 0,19,0,0
|
FILEVERSION 0,20,0,0
|
||||||
PRODUCTVERSION 0,19,0,0
|
PRODUCTVERSION 0,20,0,0
|
||||||
FILEFLAGSMASK 0x3fL
|
FILEFLAGSMASK 0x3fL
|
||||||
FILEFLAGS 0x0L
|
FILEFLAGS 0x0L
|
||||||
FILEOS 0x40004L
|
FILEOS 0x40004L
|
||||||
@@ -17,11 +17,11 @@ BEGIN
|
|||||||
BEGIN
|
BEGIN
|
||||||
VALUE "CompanyName", "NightSync"
|
VALUE "CompanyName", "NightSync"
|
||||||
VALUE "FileDescription", "NightShot Win64 screenshot utility"
|
VALUE "FileDescription", "NightShot Win64 screenshot utility"
|
||||||
VALUE "FileVersion", "0.19.0"
|
VALUE "FileVersion", "0.20.0"
|
||||||
VALUE "InternalName", "NightShot"
|
VALUE "InternalName", "NightShot"
|
||||||
VALUE "OriginalFilename", "NightShot.exe"
|
VALUE "OriginalFilename", "NightShot.exe"
|
||||||
VALUE "ProductName", "NightShot"
|
VALUE "ProductName", "NightShot"
|
||||||
VALUE "ProductVersion", "0.19.0"
|
VALUE "ProductVersion", "0.20.0"
|
||||||
END
|
END
|
||||||
END
|
END
|
||||||
BLOCK "VarFileInfo"
|
BLOCK "VarFileInfo"
|
||||||
|
|||||||
@@ -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
@@ -1,6 +1,7 @@
|
|||||||
#include "gif_recorder.hpp"
|
#include "gif_recorder.hpp"
|
||||||
|
|
||||||
#include "app_messages.hpp"
|
#include "app_messages.hpp"
|
||||||
|
#include "clipboard_history.hpp"
|
||||||
#include "image_io.hpp"
|
#include "image_io.hpp"
|
||||||
#include "settings.hpp"
|
#include "settings.hpp"
|
||||||
|
|
||||||
@@ -262,13 +263,25 @@ bool copyGifToClipboard(const Session& session) {
|
|||||||
const std::vector<BYTE> gif = readAllBytes(session.path);
|
const std::vector<BYTE> gif = readAllBytes(session.path);
|
||||||
if (gif.empty()) return false;
|
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 gifGeneric = globalBytes(gif.data(), gif.size());
|
||||||
HGLOBAL gifMime = globalBytes(gif.data(), gif.size());
|
HGLOBAL gifMime = globalBytes(gif.data(), gif.size());
|
||||||
HGLOBAL files = dropFiles(session.path);
|
HGLOBAL files = dropFiles(session.path);
|
||||||
HGLOBAL dibV5 = firstFrameDibV5(session);
|
HGLOBAL dibV5 = firstFrameDibV5(session);
|
||||||
HGLOBAL dib = firstFrameDib24(session);
|
HGLOBAL dib = firstFrameDib24(session);
|
||||||
HBITMAP bitmap = firstFrameBitmap(session);
|
HBITMAP bitmap = firstFrameBitmap(session);
|
||||||
HGLOBAL includeInHistory = serializedDword(1);
|
HGLOBAL includeInHistory = serializedDword(historyPublished ? 0u : 1u);
|
||||||
if (!gifGeneric && !gifMime && !files && !dibV5 && !dib && !bitmap) {
|
if (!gifGeneric && !gifMime && !files && !dibV5 && !dib && !bitmap) {
|
||||||
if (includeInHistory) GlobalFree(includeInHistory);
|
if (includeInHistory) GlobalFree(includeInHistory);
|
||||||
return false;
|
return false;
|
||||||
@@ -322,9 +335,9 @@ bool copyGifToClipboard(const Session& session) {
|
|||||||
transferred = true;
|
transferred = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Explicitly ask Windows 10/11 clipboard monitoring to retain this item in
|
// The WinRT history proxy above carries the first frame. Keep the rich
|
||||||
// Win+V. The first frame is also published as standard bitmap formats so
|
// animated GIF replacement out of history when that succeeded; otherwise
|
||||||
// history has a format it can display even when it ignores the custom GIF.
|
// fall back to the documented Win32 history opt-in.
|
||||||
const UINT historyFormat = RegisterClipboardFormatW(L"CanIncludeInClipboardHistory");
|
const UINT historyFormat = RegisterClipboardFormatW(L"CanIncludeInClipboardHistory");
|
||||||
if (includeInHistory && historyFormat && SetClipboardData(historyFormat, includeInHistory)) {
|
if (includeInHistory && historyFormat && SetClipboardData(historyFormat, includeInHistory)) {
|
||||||
includeInHistory = nullptr;
|
includeInHistory = nullptr;
|
||||||
|
|||||||
+48
-1
@@ -1,4 +1,5 @@
|
|||||||
#include "image_io.hpp"
|
#include "image_io.hpp"
|
||||||
|
#include "clipboard_history.hpp"
|
||||||
#include <commdlg.h>
|
#include <commdlg.h>
|
||||||
#include <shlobj.h>
|
#include <shlobj.h>
|
||||||
#include <filesystem>
|
#include <filesystem>
|
||||||
@@ -223,6 +224,39 @@ bool openClipboardWithRetry(HWND owner) {
|
|||||||
return false;
|
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 serializedDword(DWORD value) {
|
||||||
HGLOBAL memory = GlobalAlloc(GMEM_MOVEABLE, sizeof(DWORD));
|
HGLOBAL memory = GlobalAlloc(GMEM_MOVEABLE, sizeof(DWORD));
|
||||||
if (!memory) return nullptr;
|
if (!memory) return nullptr;
|
||||||
@@ -237,15 +271,28 @@ HGLOBAL serializedDword(DWORD value) {
|
|||||||
}
|
}
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
|
bool encodeBitmapForClipboardHistory(Gdiplus::Bitmap& bitmap, std::vector<BYTE>& output) {
|
||||||
|
return makeClipboardHistoryPreview(bitmap, output);
|
||||||
|
}
|
||||||
|
|
||||||
bool copyBitmapToClipboard(HWND owner, Gdiplus::Bitmap& bitmap) {
|
bool copyBitmapToClipboard(HWND owner, Gdiplus::Bitmap& bitmap) {
|
||||||
OpaqueBgraImage image = renderOpaqueBgra(bitmap);
|
OpaqueBgraImage image = renderOpaqueBgra(bitmap);
|
||||||
if (!image.valid()) return false;
|
if (!image.valid()) return false;
|
||||||
|
|
||||||
|
std::vector<BYTE> historyPreview;
|
||||||
|
const bool historyPublished =
|
||||||
|
encodeBitmapForClipboardHistory(bitmap, historyPreview) &&
|
||||||
|
clipboard_history::publishBitmap(historyPreview);
|
||||||
|
|
||||||
HGLOBAL png = makePng(image);
|
HGLOBAL png = makePng(image);
|
||||||
HGLOBAL dibV5 = makeDibV5(image);
|
HGLOBAL dibV5 = makeDibV5(image);
|
||||||
HGLOBAL dib = makeDib24(image);
|
HGLOBAL dib = makeDib24(image);
|
||||||
HBITMAP nativeBitmap = makeClipboardBitmap(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 (!png && !dibV5 && !dib && !nativeBitmap) {
|
||||||
if (includeInHistory) GlobalFree(includeInHistory);
|
if (includeInHistory) GlobalFree(includeInHistory);
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
+1
-1
@@ -209,7 +209,7 @@ std::unique_ptr<Result> performUpload(const Job& job) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
InternetHandle session(WinHttpOpen(
|
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));
|
WINHTTP_NO_PROXY_NAME, WINHTTP_NO_PROXY_BYPASS, 0));
|
||||||
if (!session.value) {
|
if (!session.value) {
|
||||||
result->message = L"WinHttpOpen: " + win32Error(GetLastError());
|
result->message = L"WinHttpOpen: " + win32Error(GetLastError());
|
||||||
|
|||||||
Reference in new issue
Block a user