Fix GIF and Overlay issues; Release NightShot v0.19
This commit is contained in:
1 parent
dfcbacfb0c
commit
ceb0b5552e
10 files changed
+528
-208
No files matched your search
@@ -7,13 +7,14 @@ NightShot — нативное приложение для Windows 10/11, пре
|
||||
## Возможности
|
||||
|
||||
- Захват произвольной области экрана через `PrintScreen`
|
||||
- Поддержка нескольких мониторов и разных значений DPI
|
||||
- Перемещение и изменение размера выделенной области
|
||||
- Поддержка нескольких мониторов и разных значений DPI без полноразмерного DIB на весь bounding box
|
||||
- Перемещение и изменение размера выделенной области только в режиме Drag
|
||||
- Аннотации живут на всём виртуальном рабочем столе независимо от области кадрирования
|
||||
- Карандаш, линии, стрелки, прямоугольники, маркер и текст
|
||||
- Настройка толщины инструментов колёсиком мыши
|
||||
- Настройка толщины инструментов колёсиком мыши с постоянным hollow-preview размера кисти
|
||||
- ColorPicker с пипеткой, HEX, RGBA и HSV
|
||||
- Сохраняемая палитра цветов 8×6
|
||||
- Копирование изображения в буфер обмена
|
||||
- Копирование изображения в буфер обмена с явным запросом сохранения в истории Win+V
|
||||
- Сохранение снимков без перезаписи существующих файлов
|
||||
- Запись GIF через `Ctrl + PrintScreen`
|
||||
- Выбор FPS, качества, курсора и лимита записи
|
||||
|
||||
+42
-11
@@ -2,26 +2,57 @@
|
||||
#include "config.hpp"
|
||||
#include <windows.h>
|
||||
|
||||
struct ScreenCapture {
|
||||
#include <cstdint>
|
||||
#include <vector>
|
||||
|
||||
struct ScreenCaptureSurface {
|
||||
HDC memoryDc = nullptr;
|
||||
HBITMAP bitmap = nullptr;
|
||||
HGDIOBJ oldBitmap = nullptr;
|
||||
void* pixels = nullptr;
|
||||
int x = 0;
|
||||
int y = 0;
|
||||
std::uint8_t* pixels = nullptr;
|
||||
RECT rect{}; // local coordinates inside the virtual-desktop window
|
||||
int width = 0;
|
||||
int height = 0;
|
||||
int stride = 0;
|
||||
|
||||
ScreenCapture() = default;
|
||||
ScreenCapture(const ScreenCapture&) = delete;
|
||||
ScreenCapture& operator=(const ScreenCapture&) = delete;
|
||||
ScreenCapture(ScreenCapture&& other) noexcept;
|
||||
ScreenCapture& operator=(ScreenCapture&& other) noexcept;
|
||||
~ScreenCapture();
|
||||
ScreenCaptureSurface() = default;
|
||||
ScreenCaptureSurface(const ScreenCaptureSurface&) = delete;
|
||||
ScreenCaptureSurface& operator=(const ScreenCaptureSurface&) = delete;
|
||||
ScreenCaptureSurface(ScreenCaptureSurface&& other) noexcept;
|
||||
ScreenCaptureSurface& operator=(ScreenCaptureSurface&& other) noexcept;
|
||||
~ScreenCaptureSurface();
|
||||
|
||||
[[nodiscard]] bool valid() const noexcept { return memoryDc && bitmap && width > 0 && height > 0; }
|
||||
[[nodiscard]] bool valid() const noexcept {
|
||||
return memoryDc && bitmap && pixels && width > 0 && height > 0;
|
||||
}
|
||||
void reset() noexcept;
|
||||
};
|
||||
|
||||
struct ScreenCapture {
|
||||
int x = 0;
|
||||
int y = 0;
|
||||
int width = 0;
|
||||
int height = 0;
|
||||
std::vector<ScreenCaptureSurface> surfaces;
|
||||
|
||||
ScreenCapture() = default;
|
||||
ScreenCapture(const ScreenCapture&) = delete;
|
||||
ScreenCapture& operator=(const ScreenCapture&) = delete;
|
||||
ScreenCapture(ScreenCapture&&) noexcept = default;
|
||||
ScreenCapture& operator=(ScreenCapture&&) noexcept = default;
|
||||
~ScreenCapture() = default;
|
||||
|
||||
[[nodiscard]] bool valid() const noexcept {
|
||||
return width > 0 && height > 0 && !surfaces.empty();
|
||||
}
|
||||
|
||||
// Copies a local virtual-desktop rectangle into destination. Empty space
|
||||
// between physically arranged monitors is filled with black instead of
|
||||
// forcing NightShot to keep one enormous bounding-box DIB in RAM.
|
||||
[[nodiscard]] bool blitTo(HDC destination, int destinationX, int destinationY,
|
||||
const RECT& sourceLocal) const noexcept;
|
||||
|
||||
[[nodiscard]] bool samplePixel(int localX, int localY, COLORREF& color) const noexcept;
|
||||
};
|
||||
|
||||
[[nodiscard]] ScreenCapture captureVirtualDesktop();
|
||||
@@ -48,6 +48,9 @@ struct AppSettings {
|
||||
int rectangleWidth = 3;
|
||||
int markerWidth = 15;
|
||||
|
||||
// Last drawing color persists independently from the saved 8x6 palette.
|
||||
std::uint32_t selectedColorArgb = 0xFF3B82F6u;
|
||||
|
||||
// ARGB values. Empty slots remain available for one-click saving from the
|
||||
// in-editor 8x6 color picker.
|
||||
std::array<std::uint32_t, kSavedColorCount> paletteArgb{};
|
||||
|
||||
+4
-4
@@ -3,8 +3,8 @@ IDI_APP_ICON ICON "nightshot.ico"
|
||||
IDR_APP_MANIFEST RT_MANIFEST "app.manifest"
|
||||
|
||||
1 VERSIONINFO
|
||||
FILEVERSION 0,17,0,0
|
||||
PRODUCTVERSION 0,17,0,0
|
||||
FILEVERSION 0,19,0,0
|
||||
PRODUCTVERSION 0,19,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.17.0"
|
||||
VALUE "FileVersion", "0.19.0"
|
||||
VALUE "InternalName", "NightShot"
|
||||
VALUE "OriginalFilename", "NightShot.exe"
|
||||
VALUE "ProductName", "NightShot"
|
||||
VALUE "ProductVersion", "0.17.0"
|
||||
VALUE "ProductVersion", "0.19.0"
|
||||
END
|
||||
END
|
||||
BLOCK "VarFileInfo"
|
||||
|
||||
+170
-27
@@ -1,17 +1,96 @@
|
||||
#include "capture.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstddef>
|
||||
#include <utility>
|
||||
|
||||
ScreenCapture::ScreenCapture(ScreenCapture&& other) noexcept { *this = std::move(other); }
|
||||
namespace {
|
||||
|
||||
ScreenCapture& ScreenCapture::operator=(ScreenCapture&& other) noexcept {
|
||||
RECT intersectRects(RECT first, RECT second) {
|
||||
RECT result{
|
||||
std::max(first.left, second.left),
|
||||
std::max(first.top, second.top),
|
||||
std::min(first.right, second.right),
|
||||
std::min(first.bottom, second.bottom)};
|
||||
if (result.right <= result.left || result.bottom <= result.top) return RECT{};
|
||||
return result;
|
||||
}
|
||||
|
||||
struct CaptureContext {
|
||||
ScreenCapture* capture = nullptr;
|
||||
HDC screenDc = nullptr;
|
||||
};
|
||||
|
||||
BOOL CALLBACK captureMonitor(HMONITOR monitor, HDC, LPRECT, LPARAM parameter) {
|
||||
auto* context = reinterpret_cast<CaptureContext*>(parameter);
|
||||
if (!context || !context->capture || !context->screenDc) return FALSE;
|
||||
|
||||
MONITORINFO info{};
|
||||
info.cbSize = sizeof(info);
|
||||
if (!GetMonitorInfoW(monitor, &info)) return TRUE;
|
||||
|
||||
const int width = info.rcMonitor.right - info.rcMonitor.left;
|
||||
const int height = info.rcMonitor.bottom - info.rcMonitor.top;
|
||||
if (width <= 0 || height <= 0) return TRUE;
|
||||
|
||||
ScreenCaptureSurface surface;
|
||||
surface.width = width;
|
||||
surface.height = height;
|
||||
surface.stride = width * 4;
|
||||
surface.rect = RECT{
|
||||
info.rcMonitor.left - context->capture->x,
|
||||
info.rcMonitor.top - context->capture->y,
|
||||
info.rcMonitor.right - context->capture->x,
|
||||
info.rcMonitor.bottom - context->capture->y};
|
||||
|
||||
surface.memoryDc = CreateCompatibleDC(context->screenDc);
|
||||
if (!surface.memoryDc) return TRUE;
|
||||
|
||||
BITMAPINFO bitmapInfo{};
|
||||
bitmapInfo.bmiHeader.biSize = sizeof(BITMAPINFOHEADER);
|
||||
bitmapInfo.bmiHeader.biWidth = width;
|
||||
bitmapInfo.bmiHeader.biHeight = -height;
|
||||
bitmapInfo.bmiHeader.biPlanes = 1;
|
||||
bitmapInfo.bmiHeader.biBitCount = 32;
|
||||
bitmapInfo.bmiHeader.biCompression = BI_RGB;
|
||||
|
||||
void* rawPixels = nullptr;
|
||||
surface.bitmap = CreateDIBSection(
|
||||
context->screenDc, &bitmapInfo, DIB_RGB_COLORS,
|
||||
&rawPixels, nullptr, 0);
|
||||
surface.pixels = static_cast<std::uint8_t*>(rawPixels);
|
||||
if (!surface.bitmap || !surface.pixels) return TRUE;
|
||||
|
||||
surface.oldBitmap = SelectObject(surface.memoryDc, surface.bitmap);
|
||||
if (!surface.oldBitmap || surface.oldBitmap == HGDI_ERROR) {
|
||||
surface.oldBitmap = nullptr;
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
if (!BitBlt(surface.memoryDc, 0, 0, width, height,
|
||||
context->screenDc, info.rcMonitor.left, info.rcMonitor.top,
|
||||
SRCCOPY | CAPTUREBLT)) {
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
context->capture->surfaces.push_back(std::move(surface));
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
ScreenCaptureSurface::ScreenCaptureSurface(ScreenCaptureSurface&& other) noexcept {
|
||||
*this = std::move(other);
|
||||
}
|
||||
|
||||
ScreenCaptureSurface& ScreenCaptureSurface::operator=(ScreenCaptureSurface&& other) noexcept {
|
||||
if (this != &other) {
|
||||
reset();
|
||||
memoryDc = std::exchange(other.memoryDc, nullptr);
|
||||
bitmap = std::exchange(other.bitmap, nullptr);
|
||||
oldBitmap = std::exchange(other.oldBitmap, nullptr);
|
||||
pixels = std::exchange(other.pixels, nullptr);
|
||||
x = std::exchange(other.x, 0);
|
||||
y = std::exchange(other.y, 0);
|
||||
rect = std::exchange(other.rect, RECT{});
|
||||
width = std::exchange(other.width, 0);
|
||||
height = std::exchange(other.height, 0);
|
||||
stride = std::exchange(other.stride, 0);
|
||||
@@ -19,19 +98,68 @@ ScreenCapture& ScreenCapture::operator=(ScreenCapture&& other) noexcept {
|
||||
return *this;
|
||||
}
|
||||
|
||||
ScreenCapture::~ScreenCapture() { reset(); }
|
||||
ScreenCaptureSurface::~ScreenCaptureSurface() {
|
||||
reset();
|
||||
}
|
||||
|
||||
void ScreenCapture::reset() noexcept {
|
||||
if (memoryDc && oldBitmap) {
|
||||
void ScreenCaptureSurface::reset() noexcept {
|
||||
if (memoryDc && oldBitmap && oldBitmap != HGDI_ERROR) {
|
||||
SelectObject(memoryDc, oldBitmap);
|
||||
}
|
||||
oldBitmap = nullptr;
|
||||
if (bitmap) DeleteObject(bitmap);
|
||||
bitmap = nullptr;
|
||||
if (memoryDc) DeleteDC(memoryDc);
|
||||
memoryDc = nullptr;
|
||||
bitmap = nullptr;
|
||||
oldBitmap = nullptr;
|
||||
pixels = nullptr;
|
||||
x = y = width = height = stride = 0;
|
||||
rect = RECT{};
|
||||
width = 0;
|
||||
height = 0;
|
||||
stride = 0;
|
||||
}
|
||||
|
||||
bool ScreenCapture::blitTo(HDC destination, int destinationX, int destinationY,
|
||||
const RECT& sourceLocal) const noexcept {
|
||||
if (!destination || sourceLocal.right <= sourceLocal.left ||
|
||||
sourceLocal.bottom <= sourceLocal.top) return false;
|
||||
|
||||
const int widthToCopy = sourceLocal.right - sourceLocal.left;
|
||||
const int heightToCopy = sourceLocal.bottom - sourceLocal.top;
|
||||
PatBlt(destination, destinationX, destinationY, widthToCopy, heightToCopy, BLACKNESS);
|
||||
|
||||
bool copied = false;
|
||||
for (const ScreenCaptureSurface& surface : surfaces) {
|
||||
if (!surface.valid()) continue;
|
||||
const RECT overlap = intersectRects(sourceLocal, surface.rect);
|
||||
if (overlap.right <= overlap.left || overlap.bottom <= overlap.top) continue;
|
||||
|
||||
const int width = overlap.right - overlap.left;
|
||||
const int height = overlap.bottom - overlap.top;
|
||||
const int sourceX = overlap.left - surface.rect.left;
|
||||
const int sourceY = overlap.top - surface.rect.top;
|
||||
const int targetX = destinationX + overlap.left - sourceLocal.left;
|
||||
const int targetY = destinationY + overlap.top - sourceLocal.top;
|
||||
if (BitBlt(destination, targetX, targetY, width, height,
|
||||
surface.memoryDc, sourceX, sourceY, SRCCOPY)) {
|
||||
copied = true;
|
||||
}
|
||||
}
|
||||
return copied;
|
||||
}
|
||||
|
||||
bool ScreenCapture::samplePixel(int localX, int localY, COLORREF& color) const noexcept {
|
||||
for (const ScreenCaptureSurface& surface : surfaces) {
|
||||
if (!surface.valid() || localX < surface.rect.left || localX >= surface.rect.right ||
|
||||
localY < surface.rect.top || localY >= surface.rect.bottom) continue;
|
||||
const int xInSurface = localX - surface.rect.left;
|
||||
const int yInSurface = localY - surface.rect.top;
|
||||
const std::uint8_t* pixel = surface.pixels +
|
||||
static_cast<std::size_t>(yInSurface) * static_cast<std::size_t>(surface.stride) +
|
||||
static_cast<std::size_t>(xInSurface) * 4u;
|
||||
color = RGB(pixel[2], pixel[1], pixel[0]);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
ScreenCapture captureVirtualDesktop() {
|
||||
@@ -40,30 +168,45 @@ ScreenCapture captureVirtualDesktop() {
|
||||
result.y = GetSystemMetrics(SM_YVIRTUALSCREEN);
|
||||
result.width = GetSystemMetrics(SM_CXVIRTUALSCREEN);
|
||||
result.height = GetSystemMetrics(SM_CYVIRTUALSCREEN);
|
||||
result.stride = result.width * 4;
|
||||
if (result.width <= 0 || result.height <= 0) return result;
|
||||
|
||||
HDC screenDc = GetDC(nullptr);
|
||||
if (!screenDc) return result;
|
||||
|
||||
result.memoryDc = CreateCompatibleDC(screenDc);
|
||||
BITMAPINFO info{};
|
||||
info.bmiHeader.biSize = sizeof(BITMAPINFOHEADER);
|
||||
info.bmiHeader.biWidth = result.width;
|
||||
info.bmiHeader.biHeight = -result.height;
|
||||
info.bmiHeader.biPlanes = 1;
|
||||
info.bmiHeader.biBitCount = 32;
|
||||
info.bmiHeader.biCompression = BI_RGB;
|
||||
CaptureContext context{&result, screenDc};
|
||||
EnumDisplayMonitors(nullptr, nullptr, captureMonitor,
|
||||
reinterpret_cast<LPARAM>(&context));
|
||||
|
||||
result.bitmap = CreateDIBSection(screenDc, &info, DIB_RGB_COLORS, &result.pixels, nullptr, 0);
|
||||
if (result.memoryDc && result.bitmap) {
|
||||
result.oldBitmap = SelectObject(result.memoryDc, result.bitmap);
|
||||
if (!BitBlt(result.memoryDc, 0, 0, result.width, result.height, screenDc,
|
||||
result.x, result.y, SRCCOPY | CAPTUREBLT)) {
|
||||
result.reset();
|
||||
// Extremely defensive fallback for unusual display drivers which don't
|
||||
// enumerate monitors through EnumDisplayMonitors.
|
||||
if (result.surfaces.empty()) {
|
||||
ScreenCaptureSurface surface;
|
||||
surface.width = result.width;
|
||||
surface.height = result.height;
|
||||
surface.stride = result.width * 4;
|
||||
surface.rect = RECT{0, 0, result.width, result.height};
|
||||
surface.memoryDc = CreateCompatibleDC(screenDc);
|
||||
|
||||
BITMAPINFO info{};
|
||||
info.bmiHeader.biSize = sizeof(BITMAPINFOHEADER);
|
||||
info.bmiHeader.biWidth = result.width;
|
||||
info.bmiHeader.biHeight = -result.height;
|
||||
info.bmiHeader.biPlanes = 1;
|
||||
info.bmiHeader.biBitCount = 32;
|
||||
info.bmiHeader.biCompression = BI_RGB;
|
||||
void* pixels = nullptr;
|
||||
surface.bitmap = surface.memoryDc
|
||||
? CreateDIBSection(screenDc, &info, DIB_RGB_COLORS, &pixels, nullptr, 0)
|
||||
: nullptr;
|
||||
surface.pixels = static_cast<std::uint8_t*>(pixels);
|
||||
if (surface.memoryDc && surface.bitmap && surface.pixels) {
|
||||
surface.oldBitmap = SelectObject(surface.memoryDc, surface.bitmap);
|
||||
if (surface.oldBitmap && surface.oldBitmap != HGDI_ERROR &&
|
||||
BitBlt(surface.memoryDc, 0, 0, result.width, result.height,
|
||||
screenDc, result.x, result.y, SRCCOPY | CAPTUREBLT)) {
|
||||
result.surfaces.push_back(std::move(surface));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
result.reset();
|
||||
}
|
||||
|
||||
ReleaseDC(nullptr, screenDc);
|
||||
|
||||
+131
-35
@@ -15,6 +15,7 @@
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
#include <condition_variable>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <cwchar>
|
||||
@@ -37,7 +38,7 @@ using Clock = std::chrono::steady_clock;
|
||||
constexpr wchar_t kHudClass[] = L"NightShotGifRecordingHud";
|
||||
constexpr COLORREF kTransparentKey = RGB(1, 2, 3);
|
||||
constexpr UINT_PTR kHudTimer = 1;
|
||||
constexpr std::size_t kMaximumQueuedFrames = 4;
|
||||
constexpr std::size_t kMaximumQueuedFrames = 2;
|
||||
#ifndef WDA_EXCLUDEFROMCAPTURE
|
||||
#define WDA_EXCLUDEFROMCAPTURE 0x00000011
|
||||
#endif
|
||||
@@ -132,6 +133,9 @@ HGLOBAL globalBytes(const void* data, std::size_t bytes) {
|
||||
return memory;
|
||||
}
|
||||
|
||||
HGLOBAL serializedDword(DWORD value) {
|
||||
return globalBytes(&value, sizeof(value));
|
||||
}
|
||||
|
||||
HGLOBAL dropFiles(const std::wstring& path) {
|
||||
const std::size_t textBytes = (path.size() + 2) * sizeof(wchar_t);
|
||||
@@ -223,6 +227,29 @@ HGLOBAL firstFrameDib24(const Session& session) {
|
||||
return memory;
|
||||
}
|
||||
|
||||
HBITMAP firstFrameBitmap(const Session& session) {
|
||||
if (session.firstFrame.empty() || session.width <= 0 || session.height <= 0) return nullptr;
|
||||
BITMAPINFO info{};
|
||||
info.bmiHeader.biSize = sizeof(BITMAPINFOHEADER);
|
||||
info.bmiHeader.biWidth = session.width;
|
||||
info.bmiHeader.biHeight = -session.height;
|
||||
info.bmiHeader.biPlanes = 1;
|
||||
info.bmiHeader.biBitCount = 32;
|
||||
info.bmiHeader.biCompression = BI_RGB;
|
||||
|
||||
void* pixels = nullptr;
|
||||
HDC screen = GetDC(nullptr);
|
||||
HBITMAP bitmap = CreateDIBSection(screen, &info, DIB_RGB_COLORS, &pixels, nullptr, 0);
|
||||
if (screen) ReleaseDC(nullptr, screen);
|
||||
if (!bitmap || !pixels) {
|
||||
if (bitmap) DeleteObject(bitmap);
|
||||
return nullptr;
|
||||
}
|
||||
std::memcpy(pixels, session.firstFrame.data(),
|
||||
static_cast<std::size_t>(session.stride) * session.height);
|
||||
return bitmap;
|
||||
}
|
||||
|
||||
bool openClipboardWithRetry(HWND owner) {
|
||||
for (int attempt = 0; attempt < 12; ++attempt) {
|
||||
if (OpenClipboard(owner)) return true;
|
||||
@@ -240,23 +267,30 @@ bool copyGifToClipboard(const Session& session) {
|
||||
HGLOBAL files = dropFiles(session.path);
|
||||
HGLOBAL dibV5 = firstFrameDibV5(session);
|
||||
HGLOBAL dib = firstFrameDib24(session);
|
||||
if (!gifGeneric && !gifMime && !files && !dibV5 && !dib) return false;
|
||||
HBITMAP bitmap = firstFrameBitmap(session);
|
||||
HGLOBAL includeInHistory = serializedDword(1);
|
||||
if (!gifGeneric && !gifMime && !files && !dibV5 && !dib && !bitmap) {
|
||||
if (includeInHistory) GlobalFree(includeInHistory);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!openClipboardWithRetry(session.request.notificationTarget)) {
|
||||
auto cleanup = [&]() {
|
||||
if (gifGeneric) GlobalFree(gifGeneric);
|
||||
if (gifMime) GlobalFree(gifMime);
|
||||
if (files) GlobalFree(files);
|
||||
if (dibV5) GlobalFree(dibV5);
|
||||
if (dib) GlobalFree(dib);
|
||||
if (bitmap) DeleteObject(bitmap);
|
||||
if (includeInHistory) GlobalFree(includeInHistory);
|
||||
};
|
||||
|
||||
if (!openClipboardWithRetry(session.request.notificationTarget)) {
|
||||
cleanup();
|
||||
return false;
|
||||
}
|
||||
if (!EmptyClipboard()) {
|
||||
CloseClipboard();
|
||||
if (gifGeneric) GlobalFree(gifGeneric);
|
||||
if (gifMime) GlobalFree(gifMime);
|
||||
if (files) GlobalFree(files);
|
||||
if (dibV5) GlobalFree(dibV5);
|
||||
if (dib) GlobalFree(dib);
|
||||
cleanup();
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -283,23 +317,96 @@ bool copyGifToClipboard(const Session& session) {
|
||||
dib = nullptr;
|
||||
transferred = true;
|
||||
}
|
||||
CloseClipboard();
|
||||
if (bitmap && SetClipboardData(CF_BITMAP, bitmap)) {
|
||||
bitmap = nullptr;
|
||||
transferred = true;
|
||||
}
|
||||
|
||||
if (gifGeneric) GlobalFree(gifGeneric);
|
||||
if (gifMime) GlobalFree(gifMime);
|
||||
if (files) GlobalFree(files);
|
||||
if (dibV5) GlobalFree(dibV5);
|
||||
if (dib) GlobalFree(dib);
|
||||
// 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.
|
||||
const UINT historyFormat = RegisterClipboardFormatW(L"CanIncludeInClipboardHistory");
|
||||
if (includeInHistory && historyFormat && SetClipboardData(historyFormat, includeInHistory)) {
|
||||
includeInHistory = nullptr;
|
||||
}
|
||||
CloseClipboard();
|
||||
cleanup();
|
||||
return transferred;
|
||||
}
|
||||
|
||||
bool ensureInfiniteLoopExtension(const std::wstring& path) {
|
||||
std::vector<BYTE> bytes = readAllBytes(path);
|
||||
static constexpr BYTE loopBlock[]{
|
||||
0x21, 0xFF, 0x0B,
|
||||
'N','E','T','S','C','A','P','E','2','.','0',
|
||||
0x03, 0x01, 0x00, 0x00, 0x00};
|
||||
if (bytes.size() < 13 ||
|
||||
!(std::memcmp(bytes.data(), "GIF87a", 6) == 0 ||
|
||||
std::memcmp(bytes.data(), "GIF89a", 6) == 0)) return false;
|
||||
|
||||
const auto found = std::search(bytes.begin(), bytes.end(),
|
||||
std::begin(loopBlock), std::end(loopBlock));
|
||||
if (found != bytes.end()) return true;
|
||||
|
||||
const BYTE packed = bytes[10];
|
||||
std::size_t insertion = 13;
|
||||
if ((packed & 0x80u) != 0) {
|
||||
const std::size_t colorCount = std::size_t{1} << ((packed & 0x07u) + 1u);
|
||||
const std::size_t tableBytes = colorCount * 3u;
|
||||
if (insertion + tableBytes > bytes.size()) return false;
|
||||
insertion += tableBytes;
|
||||
}
|
||||
|
||||
bytes.insert(bytes.begin() + static_cast<std::ptrdiff_t>(insertion),
|
||||
std::begin(loopBlock), std::end(loopBlock));
|
||||
const std::filesystem::path target(path);
|
||||
const std::filesystem::path temporary = target.wstring() + L".loopfix";
|
||||
{
|
||||
std::ofstream output(temporary, std::ios::binary | std::ios::trunc);
|
||||
if (!output) return false;
|
||||
output.write(reinterpret_cast<const char*>(bytes.data()),
|
||||
static_cast<std::streamsize>(bytes.size()));
|
||||
if (!output) return false;
|
||||
}
|
||||
if (!MoveFileExW(temporary.c_str(), target.c_str(),
|
||||
MOVEFILE_REPLACE_EXISTING | MOVEFILE_WRITE_THROUGH)) {
|
||||
DeleteFileW(temporary.c_str());
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
HRESULT setGlobalLoopMetadata(IWICBitmapEncoder* encoder) {
|
||||
if (!encoder) return E_POINTER;
|
||||
IWICMetadataQueryWriter* writer = nullptr;
|
||||
HRESULT result = encoder->GetMetadataQueryWriter(&writer);
|
||||
if (FAILED(result) || !writer) return FAILED(result) ? result : E_NOINTERFACE;
|
||||
|
||||
BYTE application[]{'N','E','T','S','C','A','P','E','2','.','0'};
|
||||
PROPVARIANT value{};
|
||||
value.vt = VT_VECTOR | VT_UI1;
|
||||
value.caub.cElems = static_cast<ULONG>(std::size(application));
|
||||
value.caub.pElems = application;
|
||||
result = writer->SetMetadataByName(L"/appext/Application", &value);
|
||||
|
||||
if (SUCCEEDED(result)) {
|
||||
BYTE data[]{0x03, 0x01, 0x00, 0x00, 0x00};
|
||||
value = PROPVARIANT{};
|
||||
value.vt = VT_VECTOR | VT_UI1;
|
||||
value.caub.cElems = static_cast<ULONG>(std::size(data));
|
||||
value.caub.pElems = data;
|
||||
result = writer->SetMetadataByName(L"/appext/Data", &value);
|
||||
}
|
||||
releaseCom(writer);
|
||||
return result;
|
||||
}
|
||||
|
||||
int paletteColorCount(int quality) {
|
||||
static constexpr int colors[10]{32, 32, 64, 64, 128, 128, 256, 256, 256, 256};
|
||||
return colors[std::clamp(quality, 1, 10) - 1];
|
||||
}
|
||||
|
||||
HRESULT setFrameMetadata(IWICBitmapFrameEncode* frame, UINT16 delay,
|
||||
bool firstFrame) {
|
||||
HRESULT setFrameMetadata(IWICBitmapFrameEncode* frame, UINT16 delay) {
|
||||
IWICMetadataQueryWriter* writer = nullptr;
|
||||
HRESULT result = frame->GetMetadataQueryWriter(&writer);
|
||||
if (FAILED(result) || !writer) return FAILED(result) ? result : E_NOINTERFACE;
|
||||
@@ -313,9 +420,6 @@ HRESULT setFrameMetadata(IWICBitmapFrameEncode* frame, UINT16 delay,
|
||||
return result;
|
||||
}
|
||||
|
||||
// These fields improve interoperability but are not required to encode a
|
||||
// valid frame. Some third-party WIC codecs reject optional metadata, so
|
||||
// only the frame delay is treated as mandatory.
|
||||
value = PROPVARIANT{};
|
||||
value.vt = VT_UI1;
|
||||
value.bVal = 1;
|
||||
@@ -326,21 +430,6 @@ HRESULT setFrameMetadata(IWICBitmapFrameEncode* frame, UINT16 delay,
|
||||
value.boolVal = VARIANT_FALSE;
|
||||
writer->SetMetadataByName(L"/grctlext/TransparencyFlag", &value);
|
||||
|
||||
if (firstFrame) {
|
||||
static char application[] = "NETSCAPE2.0";
|
||||
value = PROPVARIANT{};
|
||||
value.vt = VT_LPSTR;
|
||||
value.pszVal = application;
|
||||
writer->SetMetadataByName(L"/appext/Application", &value);
|
||||
|
||||
BYTE loopData[4]{3, 1, 0, 0};
|
||||
value = PROPVARIANT{};
|
||||
value.vt = VT_VECTOR | VT_UI1;
|
||||
value.caub.cElems = static_cast<ULONG>(std::size(loopData));
|
||||
value.caub.pElems = loopData;
|
||||
writer->SetMetadataByName(L"/appext/Data", &value);
|
||||
}
|
||||
|
||||
releaseCom(writer);
|
||||
return S_OK;
|
||||
}
|
||||
@@ -358,6 +447,7 @@ public:
|
||||
width_ = width;
|
||||
height_ = height;
|
||||
quality_ = std::clamp(quality, 1, 10);
|
||||
path_ = path;
|
||||
|
||||
HRESULT result = CoCreateInstance(
|
||||
CLSID_WICImagingFactory, nullptr, CLSCTX_INPROC_SERVER,
|
||||
@@ -386,6 +476,7 @@ public:
|
||||
error = L"Не удалось инициализировать GIF encoder: " + hresultText(result);
|
||||
return false;
|
||||
}
|
||||
(void)setGlobalLoopMetadata(encoder_);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -473,7 +564,7 @@ public:
|
||||
}
|
||||
if (SUCCEEDED(result)) {
|
||||
stage = L"GIF metadata";
|
||||
result = setFrameMetadata(encodedFrame, delay, frameCount_ == 0);
|
||||
result = setFrameMetadata(encodedFrame, delay);
|
||||
}
|
||||
if (SUCCEEDED(result)) {
|
||||
// Let WIC convert BGRA directly into the indexed encoder format.
|
||||
@@ -518,6 +609,10 @@ public:
|
||||
releaseCom(encoder_);
|
||||
releaseCom(stream_);
|
||||
releaseCom(factory_);
|
||||
if (!ensureInfiniteLoopExtension(path_)) {
|
||||
error = L"GIF сохранён, но не удалось гарантировать бесконечное зацикливание.";
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -529,6 +624,7 @@ private:
|
||||
int height_ = 0;
|
||||
int quality_ = 8;
|
||||
int frameCount_ = 0;
|
||||
std::wstring path_;
|
||||
};
|
||||
|
||||
void drawCursorIntoCapture(HDC destination, const RECT& sourceRect) {
|
||||
|
||||
+25
-1
@@ -222,6 +222,19 @@ bool openClipboardWithRetry(HWND owner) {
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
HGLOBAL serializedDword(DWORD value) {
|
||||
HGLOBAL memory = GlobalAlloc(GMEM_MOVEABLE, sizeof(DWORD));
|
||||
if (!memory) return nullptr;
|
||||
void* destination = GlobalLock(memory);
|
||||
if (!destination) {
|
||||
GlobalFree(memory);
|
||||
return nullptr;
|
||||
}
|
||||
std::memcpy(destination, &value, sizeof(value));
|
||||
GlobalUnlock(memory);
|
||||
return memory;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
bool copyBitmapToClipboard(HWND owner, Gdiplus::Bitmap& bitmap) {
|
||||
@@ -232,13 +245,18 @@ bool copyBitmapToClipboard(HWND owner, Gdiplus::Bitmap& bitmap) {
|
||||
HGLOBAL dibV5 = makeDibV5(image);
|
||||
HGLOBAL dib = makeDib24(image);
|
||||
HBITMAP nativeBitmap = makeClipboardBitmap(image);
|
||||
if (!png && !dibV5 && !dib && !nativeBitmap) return false;
|
||||
HGLOBAL includeInHistory = serializedDword(1);
|
||||
if (!png && !dibV5 && !dib && !nativeBitmap) {
|
||||
if (includeInHistory) GlobalFree(includeInHistory);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!openClipboardWithRetry(owner)) {
|
||||
if (png) GlobalFree(png);
|
||||
if (dibV5) GlobalFree(dibV5);
|
||||
if (dib) GlobalFree(dib);
|
||||
if (nativeBitmap) DeleteObject(nativeBitmap);
|
||||
if (includeInHistory) GlobalFree(includeInHistory);
|
||||
return false;
|
||||
}
|
||||
if (!EmptyClipboard()) {
|
||||
@@ -247,6 +265,7 @@ bool copyBitmapToClipboard(HWND owner, Gdiplus::Bitmap& bitmap) {
|
||||
if (dibV5) GlobalFree(dibV5);
|
||||
if (dib) GlobalFree(dib);
|
||||
if (nativeBitmap) DeleteObject(nativeBitmap);
|
||||
if (includeInHistory) GlobalFree(includeInHistory);
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -268,12 +287,17 @@ bool copyBitmapToClipboard(HWND owner, Gdiplus::Bitmap& bitmap) {
|
||||
nativeBitmap = nullptr;
|
||||
transferred = true;
|
||||
}
|
||||
const UINT historyFormat = RegisterClipboardFormatW(L"CanIncludeInClipboardHistory");
|
||||
if (includeInHistory && historyFormat && SetClipboardData(historyFormat, includeInHistory)) {
|
||||
includeInHistory = nullptr;
|
||||
}
|
||||
CloseClipboard();
|
||||
|
||||
if (png) GlobalFree(png);
|
||||
if (dibV5) GlobalFree(dibV5);
|
||||
if (dib) GlobalFree(dib);
|
||||
if (nativeBitmap) DeleteObject(nativeBitmap);
|
||||
if (includeInHistory) GlobalFree(includeInHistory);
|
||||
return transferred;
|
||||
}
|
||||
|
||||
|
||||
+140
-125
@@ -141,19 +141,34 @@ Tool toolFromAction(Action action) {
|
||||
}
|
||||
}
|
||||
|
||||
void drawArrow(Graphics& g, const Pen& pen, PointF start, PointF end) {
|
||||
g.DrawLine(&pen, start, end);
|
||||
void drawArrow(Graphics& g, const Pen& pen, const Color& color, PointF start, PointF end) {
|
||||
const float dx = end.X - start.X;
|
||||
const float dy = end.Y - start.Y;
|
||||
const float len = std::sqrt(dx * dx + dy * dy);
|
||||
if (len < 1.0f) return;
|
||||
const float angle = std::atan2(dy, dx);
|
||||
const float head = std::clamp(len * 0.22f, 9.0f, 18.0f);
|
||||
const float spread = 0.55f;
|
||||
PointF a(end.X - head * std::cos(angle - spread), end.Y - head * std::sin(angle - spread));
|
||||
PointF b(end.X - head * std::cos(angle + spread), end.Y - head * std::sin(angle + spread));
|
||||
g.DrawLine(&pen, end, a);
|
||||
g.DrawLine(&pen, end, b);
|
||||
const float length = std::sqrt(dx * dx + dy * dy);
|
||||
if (length < 1.0f) return;
|
||||
|
||||
const float ux = dx / length;
|
||||
const float uy = dy / length;
|
||||
const float nx = -uy;
|
||||
const float ny = ux;
|
||||
const float stroke = std::max(1.0f, pen.GetWidth());
|
||||
const float headLength = std::clamp(stroke * 4.6f + 7.0f, 11.0f, 27.0f);
|
||||
const float halfHeadWidth = std::clamp(headLength * 0.48f, 5.0f, 14.0f);
|
||||
|
||||
// Lightshot-style arrow: one clean rounded shaft with a filled triangular
|
||||
// head. The shaft runs under the head slightly so antialiasing never leaves
|
||||
// a visible seam between the two shapes.
|
||||
const float shaftInset = std::min(headLength * 0.58f, length * 0.45f);
|
||||
PointF shaftEnd(end.X - ux * shaftInset, end.Y - uy * shaftInset);
|
||||
g.DrawLine(&pen, start, shaftEnd);
|
||||
|
||||
PointF baseCenter(end.X - ux * headLength, end.Y - uy * headLength);
|
||||
PointF triangle[3]{
|
||||
end,
|
||||
PointF(baseCenter.X + nx * halfHeadWidth, baseCenter.Y + ny * halfHeadWidth),
|
||||
PointF(baseCenter.X - nx * halfHeadWidth, baseCenter.Y - ny * halfHeadWidth)};
|
||||
SolidBrush headBrush(color);
|
||||
g.FillPolygon(&headBrush, triangle, 3);
|
||||
}
|
||||
|
||||
void drawAnnotation(Graphics& g, const Annotation& a, float offsetX, float offsetY) {
|
||||
@@ -184,7 +199,7 @@ void drawAnnotation(Graphics& g, const Annotation& a, float offsetX, float offse
|
||||
if (a.points.size() >= 2) g.DrawLine(&pen, shifted(a.points[0]), shifted(a.points[1]));
|
||||
break;
|
||||
case Tool::Arrow:
|
||||
if (a.points.size() >= 2) drawArrow(g, pen, shifted(a.points[0]), shifted(a.points[1]));
|
||||
if (a.points.size() >= 2) drawArrow(g, pen, a.color, shifted(a.points[0]), shifted(a.points[1]));
|
||||
break;
|
||||
case Tool::Rectangle:
|
||||
if (a.points.size() >= 2) {
|
||||
@@ -231,7 +246,7 @@ void drawAnnotationClipped(Graphics& g, const Annotation& annotation, const RECT
|
||||
maxY = std::max(maxY, point.Y + offsetY);
|
||||
}
|
||||
float padding = annotation.width * 0.5f + 5.0f;
|
||||
if (annotation.tool == Tool::Arrow) padding += 19.0f;
|
||||
if (annotation.tool == Tool::Arrow) padding += 30.0f;
|
||||
if (annotation.tool == Tool::Text) {
|
||||
const float size = std::max(14.0f, annotation.textSize);
|
||||
maxX += std::max(size * 2.0f, static_cast<float>(annotation.text.size()) * size * 0.72f);
|
||||
@@ -562,8 +577,8 @@ struct OverlayWindow::Impl {
|
||||
markerWidth = static_cast<float>(stored.markerWidth);
|
||||
savedPalette = stored.paletteArgb;
|
||||
savedPaletteOccupied = stored.paletteOccupied;
|
||||
color = color_picker_math::colorFromArgb(savedPalette[0]);
|
||||
colorAlpha = color_picker_math::alphaFromArgb(savedPalette[0]);
|
||||
color = color_picker_math::colorFromArgb(stored.selectedColorArgb);
|
||||
colorAlpha = color_picker_math::alphaFromArgb(stored.selectedColorArgb);
|
||||
pickerHsv = color_picker_math::rgbToHsv(GetRValue(color), GetGValue(color), GetBValue(color));
|
||||
|
||||
POINT cursor{};
|
||||
@@ -600,8 +615,6 @@ struct OverlayWindow::Impl {
|
||||
Tool tool = Tool::Drag;
|
||||
COLORREF color = theme::kAccent;
|
||||
std::vector<Annotation> annotations;
|
||||
LONG appliedSelectionMoveDx = 0;
|
||||
LONG appliedSelectionMoveDy = 0;
|
||||
std::vector<Button> verticalButtons;
|
||||
std::vector<Button> horizontalButtons;
|
||||
std::array<RECT, 8> handleRects{};
|
||||
@@ -797,6 +810,7 @@ struct OverlayWindow::Impl {
|
||||
value.arrowWidth = static_cast<int>(std::lround(arrowWidth));
|
||||
value.rectangleWidth = static_cast<int>(std::lround(rectangleWidth));
|
||||
value.markerWidth = static_cast<int>(std::lround(markerWidth));
|
||||
value.selectedColorArgb = currentArgb();
|
||||
value.paletteArgb = savedPalette;
|
||||
value.paletteOccupied = savedPaletteOccupied;
|
||||
const bool saved = settings::save(value);
|
||||
@@ -837,6 +851,7 @@ struct OverlayWindow::Impl {
|
||||
color = nextColor;
|
||||
colorAlpha = nextAlpha;
|
||||
if (syncHsv) syncPickerFromRgb();
|
||||
persistEditorPreferences();
|
||||
}
|
||||
|
||||
std::uint32_t currentArgb() const {
|
||||
@@ -929,22 +944,22 @@ struct OverlayWindow::Impl {
|
||||
changed = true;
|
||||
break;
|
||||
case ColorField::Alpha:
|
||||
colorAlpha = static_cast<BYTE>(std::clamp(value, 0, 255));
|
||||
setCurrentColor(color, static_cast<BYTE>(std::clamp(value, 0, 255)), false);
|
||||
changed = true;
|
||||
break;
|
||||
case ColorField::Hue:
|
||||
pickerHsv.h = static_cast<float>((value % 360 + 360) % 360);
|
||||
color = color_picker_math::hsvToRgb(pickerHsv.h, pickerHsv.s, pickerHsv.v);
|
||||
setCurrentColor(color_picker_math::hsvToRgb(pickerHsv.h, pickerHsv.s, pickerHsv.v), colorAlpha, false);
|
||||
changed = true;
|
||||
break;
|
||||
case ColorField::Saturation:
|
||||
pickerHsv.s = static_cast<float>(std::clamp(value, 0, 100)) / 100.0f;
|
||||
color = color_picker_math::hsvToRgb(pickerHsv.h, pickerHsv.s, pickerHsv.v);
|
||||
setCurrentColor(color_picker_math::hsvToRgb(pickerHsv.h, pickerHsv.s, pickerHsv.v), colorAlpha, false);
|
||||
changed = true;
|
||||
break;
|
||||
case ColorField::Value:
|
||||
pickerHsv.v = static_cast<float>(std::clamp(value, 0, 100)) / 100.0f;
|
||||
color = color_picker_math::hsvToRgb(pickerHsv.h, pickerHsv.s, pickerHsv.v);
|
||||
setCurrentColor(color_picker_math::hsvToRgb(pickerHsv.h, pickerHsv.s, pickerHsv.v), colorAlpha, false);
|
||||
changed = true;
|
||||
break;
|
||||
default: break;
|
||||
@@ -1051,19 +1066,19 @@ struct OverlayWindow::Impl {
|
||||
}
|
||||
|
||||
bool sampleCapturedColor(POINT point) {
|
||||
if (!capture.pixels || point.x < 0 || point.y < 0 ||
|
||||
if (point.x < 0 || point.y < 0 ||
|
||||
point.x >= capture.width || point.y >= capture.height) return false;
|
||||
const auto* row = static_cast<const std::uint8_t*>(capture.pixels) +
|
||||
static_cast<std::size_t>(point.y) * static_cast<std::size_t>(capture.stride);
|
||||
const auto* pixel = row + static_cast<std::size_t>(point.x) * 4u;
|
||||
setCurrentColor(RGB(pixel[2], pixel[1], pixel[0]), 255);
|
||||
COLORREF sampled{};
|
||||
if (!capture.samplePixel(point.x, point.y, sampled)) return false;
|
||||
setCurrentColor(sampled, 255);
|
||||
return true;
|
||||
}
|
||||
|
||||
RECT brushPreviewBounds(POINT point) const {
|
||||
if (!hasSelection || !isDrawingTool(tool) || !pointInRect(selection, point)) return RECT{0, 0, 0, 0};
|
||||
const int diameter = std::max(3, static_cast<int>(std::ceil(currentStrokeWidth())));
|
||||
const int radius = diameter / 2 + 4;
|
||||
if (!hasSelection || !isDrawingTool(tool) || point.x < 0 || point.y < 0 ||
|
||||
point.x >= capture.width || point.y >= capture.height) return RECT{0, 0, 0, 0};
|
||||
const int diameter = std::max(1, static_cast<int>(std::ceil(currentStrokeWidth())));
|
||||
const int radius = (diameter + 1) / 2 + 2;
|
||||
return clampToClient(RECT{point.x - radius, point.y - radius,
|
||||
point.x + radius + 1, point.y + radius + 1});
|
||||
}
|
||||
@@ -1082,7 +1097,7 @@ struct OverlayWindow::Impl {
|
||||
const float step = tool == Tool::Marker ? 2.0f : 1.0f;
|
||||
const float maximum = tool == Tool::Marker ? 96.0f : 64.0f;
|
||||
const RECT previous = brushPreviewRect;
|
||||
*width = std::clamp(*width + (wheelDelta > 0 ? step : -step), 1.0f, maximum);
|
||||
*width = std::clamp(*width + (wheelDelta > 0 ? -step : step), 1.0f, maximum);
|
||||
persistEditorPreferences();
|
||||
brushPreviewRect = brushPreviewBounds(cursorPoint);
|
||||
invalidateRectPadded(previous, 3);
|
||||
@@ -1333,7 +1348,7 @@ struct OverlayWindow::Impl {
|
||||
maxY = std::max(maxY, point.Y);
|
||||
}
|
||||
int padding = static_cast<int>(std::ceil(annotation.width * 0.5f)) + 5;
|
||||
if (annotation.tool == Tool::Arrow) padding += 19;
|
||||
if (annotation.tool == Tool::Arrow) padding += 30;
|
||||
if (annotation.tool == Tool::Text) {
|
||||
const float size = std::max(14.0f, annotation.textSize);
|
||||
maxX += std::max(size * 2.0f, static_cast<float>(annotation.text.size()) * size * 0.72f);
|
||||
@@ -1726,6 +1741,7 @@ struct OverlayWindow::Impl {
|
||||
pickerDraggingSv = false;
|
||||
pickerDraggingHue = false;
|
||||
pickerDraggingAlpha = false;
|
||||
persistEditorPreferences();
|
||||
ReleaseCapture();
|
||||
}
|
||||
|
||||
@@ -1806,16 +1822,24 @@ struct OverlayWindow::Impl {
|
||||
|
||||
std::unique_ptr<Bitmap> renderSelectionBitmap() {
|
||||
if (!hasSelection || rectWidth(selection) <= 0 || rectHeight(selection) <= 0) return {};
|
||||
auto output = std::make_unique<Bitmap>(rectWidth(selection), rectHeight(selection), PixelFormat32bppARGB);
|
||||
// Screenshots are opaque. RGB avoids the zero-alpha semantics of GDI
|
||||
// writes into an ARGB GDI+ bitmap while the sparse monitor capture is
|
||||
// copied into this temporary crop.
|
||||
auto output = std::make_unique<Bitmap>(rectWidth(selection), rectHeight(selection), PixelFormat32bppRGB);
|
||||
Graphics g(output.get());
|
||||
g.Clear(Color(255, 255, 255, 255));
|
||||
g.Clear(Color(255, 0, 0, 0));
|
||||
HDC outputDc = g.GetHDC();
|
||||
if (!outputDc) return {};
|
||||
const bool copied = capture.blitTo(outputDc, 0, 0, selection);
|
||||
g.ReleaseHDC(outputDc);
|
||||
if (!copied) return {};
|
||||
|
||||
g.SetSmoothingMode(SmoothingModeAntiAlias);
|
||||
g.SetTextRenderingHint(TextRenderingHintAntiAliasGridFit);
|
||||
Bitmap source(capture.bitmap, nullptr);
|
||||
Rect destination(0, 0, rectWidth(selection), rectHeight(selection));
|
||||
g.DrawImage(&source, destination, selection.left, selection.top,
|
||||
rectWidth(selection), rectHeight(selection), UnitPixel);
|
||||
const Rect destination(0, 0, rectWidth(selection), rectHeight(selection));
|
||||
g.SetClip(destination);
|
||||
// Annotations live in virtual-desktop coordinates. Selection is only a
|
||||
// crop rectangle, so moving/resizing it never mutates drawing data.
|
||||
for (const auto& annotation : annotations)
|
||||
drawAnnotation(g, annotation, static_cast<float>(-selection.left), static_cast<float>(-selection.top));
|
||||
return output;
|
||||
@@ -2418,44 +2442,35 @@ struct OverlayWindow::Impl {
|
||||
}
|
||||
|
||||
void drawBrushPreview(Graphics& g, RECT dirty) {
|
||||
if (!hasSelection || selecting || drawing || draggingSelection || resizeHandle >= 0 ||
|
||||
if (!hasSelection || selecting || draggingSelection || resizeHandle >= 0 ||
|
||||
paletteOpen || eyedropperActive || !isDrawingTool(tool) ||
|
||||
!rectsIntersect(brushPreviewRect, dirty) ||
|
||||
!pointInRect(selection, cursorPoint)) return;
|
||||
const float width = std::max(1.0f, currentStrokeWidth());
|
||||
const float visualDiameter = std::max(4.0f, width);
|
||||
const float x = static_cast<float>(cursorPoint.x) - visualDiameter * 0.5f;
|
||||
const float y = static_cast<float>(cursorPoint.y) - visualDiameter * 0.5f;
|
||||
!rectsIntersect(brushPreviewRect, dirty)) return;
|
||||
|
||||
const float diameter = std::max(1.0f, currentStrokeWidth());
|
||||
const float x = static_cast<float>(cursorPoint.x) - diameter * 0.5f;
|
||||
const float y = static_cast<float>(cursorPoint.y) - diameter * 0.5f;
|
||||
const GraphicsState state = g.Save();
|
||||
g.SetSmoothingMode(SmoothingModeAntiAlias);
|
||||
g.SetPixelOffsetMode(PixelOffsetModeHalf);
|
||||
SolidBrush fill(Color(tool == Tool::Marker ? std::min<int>(colorAlpha, 96) : std::max<int>(70, colorAlpha),
|
||||
GetRValue(color), GetGValue(color), GetBValue(color)));
|
||||
g.FillEllipse(&fill, x, y, visualDiameter, visualDiameter);
|
||||
Pen shadow(Color(205, 0, 0, 0), 3.0f);
|
||||
Pen border(Color(245, 255, 255, 255), 1.0f);
|
||||
g.DrawEllipse(&shadow, x, y, visualDiameter, visualDiameter);
|
||||
g.DrawEllipse(&border, x, y, visualDiameter, visualDiameter);
|
||||
if (width < 4.0f) {
|
||||
SolidBrush center(toGdiColor(color, colorAlpha));
|
||||
const float centerSize = std::max(1.0f, width);
|
||||
g.FillEllipse(¢er,
|
||||
static_cast<float>(cursorPoint.x) - centerSize * 0.5f,
|
||||
static_cast<float>(cursorPoint.y) - centerSize * 0.5f,
|
||||
centerSize, centerSize);
|
||||
}
|
||||
|
||||
// Preview is deliberately hollow: it represents the exact physical
|
||||
// brush diameter without covering the pixels the user is trying to aim
|
||||
// at. Keep the border at one physical pixel on every monitor.
|
||||
Pen border(toGdiColor(color, static_cast<BYTE>(std::max<int>(colorAlpha, 160))), 1.0f);
|
||||
g.DrawEllipse(&border, x, y, diameter, diameter);
|
||||
g.Restore(state);
|
||||
}
|
||||
|
||||
void drawScene(Graphics& g, RECT dirty) {
|
||||
// Drawing is a virtual-desktop layer, independent from the crop. It is
|
||||
// intentionally visible even outside the selected rectangle and keeps
|
||||
// absolute coordinates while the selection moves or resizes.
|
||||
for (const auto& annotation : annotations) {
|
||||
drawAnnotationClipped(g, annotation, dirty);
|
||||
}
|
||||
|
||||
if (hasSelection || selecting) {
|
||||
const RECT r = selection;
|
||||
const GraphicsState annotationState = g.Save();
|
||||
g.SetClip(Rect(r.left, r.top, std::max(0, rectWidth(r)), std::max(0, rectHeight(r))), CombineModeIntersect);
|
||||
for (const auto& annotation : annotations) {
|
||||
drawAnnotationClipped(g, annotation, dirty);
|
||||
}
|
||||
g.Restore(annotationState);
|
||||
|
||||
const int borderPadding = std::max(10, ui.handle + 6);
|
||||
const std::array<RECT, 4> borderBands{{
|
||||
@@ -2473,7 +2488,7 @@ struct OverlayWindow::Impl {
|
||||
static_cast<REAL>(std::max(0, rectWidth(r) - 1)),
|
||||
static_cast<REAL>(std::max(0, rectHeight(r) - 1)));
|
||||
|
||||
if (hasSelection && !selecting) {
|
||||
if (hasSelection && !selecting && tool == Tool::Drag) {
|
||||
SolidBrush handleFill(toGdiColor(theme::kAccentHover));
|
||||
Pen handleBorder(Color(210, 255, 255, 255), 1.0f);
|
||||
for (const RECT& handle : handleRects) {
|
||||
@@ -2597,7 +2612,7 @@ struct OverlayWindow::Impl {
|
||||
const int height = rectHeight(tile);
|
||||
if (width <= 0 || height <= 0 || !ensureScratchBuffer(target, width, height)) return;
|
||||
|
||||
BitBlt(scratchDc, 0, 0, width, height, capture.memoryDc, tile.left, tile.top, SRCCOPY);
|
||||
static_cast<void>(capture.blitTo(scratchDc, 0, 0, tile));
|
||||
dimScratchRect(tile);
|
||||
|
||||
Graphics graphics(scratchDc);
|
||||
@@ -2666,11 +2681,11 @@ struct OverlayWindow::Impl {
|
||||
const int padding = static_cast<int>(std::ceil(annotation.width * 0.5f)) + 4;
|
||||
if ((annotation.tool == Tool::Line || annotation.tool == Tool::Arrow) && annotation.points.size() >= 2) {
|
||||
invalidateSegmentTiles(annotation.points[0], annotation.points[1],
|
||||
padding + (annotation.tool == Tool::Arrow ? 10 : 0));
|
||||
padding + (annotation.tool == Tool::Arrow ? 30 : 0));
|
||||
if (annotation.tool == Tool::Arrow) {
|
||||
const PointF end = annotation.points[1];
|
||||
invalidateRectPadded(RECT{static_cast<LONG>(end.X) - 24, static_cast<LONG>(end.Y) - 24,
|
||||
static_cast<LONG>(end.X) + 25, static_cast<LONG>(end.Y) + 25}, 1);
|
||||
invalidateRectPadded(RECT{static_cast<LONG>(end.X) - 36, static_cast<LONG>(end.Y) - 36,
|
||||
static_cast<LONG>(end.X) + 37, static_cast<LONG>(end.Y) + 37}, 1);
|
||||
}
|
||||
} else if (annotation.tool == Tool::Rectangle && annotation.points.size() >= 2) {
|
||||
invalidateRectangleOutline(annotation.points[0], annotation.points[1], padding);
|
||||
@@ -2680,7 +2695,9 @@ struct OverlayWindow::Impl {
|
||||
}
|
||||
|
||||
void beginAnnotation(POINT p) {
|
||||
if (tool == Tool::Drag || !pointInRect(selection, p)) return;
|
||||
if (!hasSelection || tool == Tool::Drag) return;
|
||||
p.x = std::clamp(p.x, 0L, static_cast<LONG>(capture.width - 1));
|
||||
p.y = std::clamp(p.y, 0L, static_cast<LONG>(capture.height - 1));
|
||||
if (tool == Tool::Text) {
|
||||
beginTextEdit(p);
|
||||
return;
|
||||
@@ -2701,8 +2718,8 @@ struct OverlayWindow::Impl {
|
||||
|
||||
void updateAnnotation(POINT p) {
|
||||
if (!drawing || annotations.empty()) return;
|
||||
p.x = std::clamp(p.x, selection.left, selection.right);
|
||||
p.y = std::clamp(p.y, selection.top, selection.bottom);
|
||||
p.x = std::clamp(p.x, 0L, static_cast<LONG>(capture.width - 1));
|
||||
p.y = std::clamp(p.y, 0L, static_cast<LONG>(capture.height - 1));
|
||||
Annotation& annotation = annotations.back();
|
||||
|
||||
if (annotation.tool == Tool::Pen || annotation.tool == Tool::Marker) {
|
||||
@@ -2746,7 +2763,7 @@ struct OverlayWindow::Impl {
|
||||
if (!textEditing) return;
|
||||
const int monitorIndex = monitorIndexForPoint(textAnchor);
|
||||
const UiMetrics metrics = metricsForMonitor(monitorIndex);
|
||||
RECT safe = intersectRects(monitors[monitorIndex].workBounds, selection);
|
||||
RECT safe = monitors[monitorIndex].workBounds;
|
||||
RECT insetSafe = safe;
|
||||
InflateRect(&insetSafe, -metrics.margin, -metrics.margin);
|
||||
if (rectWidth(insetSafe) >= 40 && rectHeight(insetSafe) >= 24) safe = insetSafe;
|
||||
@@ -2779,7 +2796,7 @@ struct OverlayWindow::Impl {
|
||||
commitTextEdit();
|
||||
const int monitorIndex = monitorIndexForPoint(p);
|
||||
const UiMetrics metrics = metricsForMonitor(monitorIndex);
|
||||
RECT available = intersectRects(monitors[monitorIndex].workBounds, selection);
|
||||
RECT available = monitors[monitorIndex].workBounds;
|
||||
InflateRect(&available, -metrics.margin, -metrics.margin);
|
||||
if (rectWidth(available) < 50 || rectHeight(available) < 32) return;
|
||||
|
||||
@@ -2900,9 +2917,6 @@ struct OverlayWindow::Impl {
|
||||
const RECT previousUi = uiBounds();
|
||||
const RECT previousBadge = dimensionBadgeRect;
|
||||
const RECT previousPreview = brushPreviewRect;
|
||||
if (!annotations.empty()) {
|
||||
for (const auto& annotation : annotations) invalidateAnnotationShape(annotation);
|
||||
}
|
||||
|
||||
const LONG requestedDx = p.x - dragStart.x;
|
||||
const LONG requestedDy = p.y - dragStart.y;
|
||||
@@ -2910,29 +2924,11 @@ struct OverlayWindow::Impl {
|
||||
OffsetRect(&nextLogical, requestedDx, requestedDy);
|
||||
nextLogical = keepLogicalSelectionReachable(nextLogical);
|
||||
|
||||
const LONG actualDx = nextLogical.left - originalLogicalSelection.left;
|
||||
const LONG actualDy = nextLogical.top - originalLogicalSelection.top;
|
||||
logicalSelection = nextLogical;
|
||||
selection = visibleSelectionFromLogical(logicalSelection);
|
||||
|
||||
const LONG deltaDx = actualDx - appliedSelectionMoveDx;
|
||||
const LONG deltaDy = actualDy - appliedSelectionMoveDy;
|
||||
if (deltaDx != 0 || deltaDy != 0) {
|
||||
for (auto& annotation : annotations) {
|
||||
for (auto& point : annotation.points) {
|
||||
point.X += static_cast<REAL>(deltaDx);
|
||||
point.Y += static_cast<REAL>(deltaDy);
|
||||
}
|
||||
for (RECT& bounds : annotation.strokeChunkBounds) OffsetRect(&bounds, deltaDx, deltaDy);
|
||||
if (annotation.hasStrokeBounds) OffsetRect(&annotation.strokeBounds, deltaDx, deltaDy);
|
||||
}
|
||||
appliedSelectionMoveDx = actualDx;
|
||||
appliedSelectionMoveDy = actualDy;
|
||||
}
|
||||
|
||||
if (!annotations.empty()) {
|
||||
for (const auto& annotation : annotations) invalidateAnnotationShape(annotation);
|
||||
}
|
||||
// Annotations deliberately stay in absolute virtual-desktop
|
||||
// coordinates. Selection is only the crop rectangle.
|
||||
updateLayout();
|
||||
brushPreviewRect = brushPreviewBounds(cursorPoint);
|
||||
invalidateSelectionTransition(previousSelection, previousUi, previousBadge);
|
||||
@@ -2955,26 +2951,41 @@ struct OverlayWindow::Impl {
|
||||
if (pointInRect(pickerEyedropperRect, p)) { SetCursor(LoadCursorW(nullptr, IDC_HAND)); return; }
|
||||
}
|
||||
if (!hasSelection) { SetCursor(LoadCursorW(nullptr, IDC_CROSS)); return; }
|
||||
const int handle = hitHandle(p);
|
||||
switch (handle) {
|
||||
case 0: case 4: SetCursor(LoadCursorW(nullptr, IDC_SIZENWSE)); return;
|
||||
case 2: case 6: SetCursor(LoadCursorW(nullptr, IDC_SIZENESW)); return;
|
||||
case 1: case 5: SetCursor(LoadCursorW(nullptr, IDC_SIZENS)); return;
|
||||
case 3: case 7: SetCursor(LoadCursorW(nullptr, IDC_SIZEWE)); return;
|
||||
default: break;
|
||||
}
|
||||
if (hitButton(p) >= 0 || hitPalette(p)) { SetCursor(LoadCursorW(nullptr, IDC_HAND)); return; }
|
||||
if (hitToolbarPanel(p)) { SetCursor(LoadCursorW(nullptr, IDC_ARROW)); return; }
|
||||
if (hitSelectionBorder(p)) { SetCursor(LoadCursorW(nullptr, IDC_SIZEALL)); return; }
|
||||
if (pointInRect(selection, p)) {
|
||||
if (tool == Tool::Drag) SetCursor(LoadCursorW(nullptr, IDC_SIZEALL));
|
||||
else if (tool == Tool::Text) SetCursor(LoadCursorW(nullptr, IDC_IBEAM));
|
||||
else if (isDrawingTool(tool)) SetCursor(nullptr);
|
||||
else SetCursor(LoadCursorW(nullptr, IDC_CROSS));
|
||||
|
||||
// The selection is interactive only in Drag mode. Drawing tools never
|
||||
// accidentally resize/move the crop when a stroke starts near its edge.
|
||||
if (tool == Tool::Drag) {
|
||||
const int handle = hitHandle(p);
|
||||
switch (handle) {
|
||||
case 0: case 4: SetCursor(LoadCursorW(nullptr, IDC_SIZENWSE)); return;
|
||||
case 2: case 6: SetCursor(LoadCursorW(nullptr, IDC_SIZENESW)); return;
|
||||
case 1: case 5: SetCursor(LoadCursorW(nullptr, IDC_SIZENS)); return;
|
||||
case 3: case 7: SetCursor(LoadCursorW(nullptr, IDC_SIZEWE)); return;
|
||||
default: break;
|
||||
}
|
||||
if (hitSelectionBorder(p) || pointInRect(selection, p)) {
|
||||
SetCursor(LoadCursorW(nullptr, IDC_SIZEALL));
|
||||
return;
|
||||
}
|
||||
SetCursor(LoadCursorW(nullptr, IDC_ARROW));
|
||||
return;
|
||||
}
|
||||
SetCursor(LoadCursorW(nullptr, IDC_ARROW));
|
||||
|
||||
if (tool == Tool::Text) {
|
||||
SetCursor(LoadCursorW(nullptr, IDC_IBEAM));
|
||||
return;
|
||||
}
|
||||
if (isDrawingTool(tool)) {
|
||||
// The hollow brush preview becomes the cursor over the entire
|
||||
// virtual desktop, not just over the crop.
|
||||
SetCursor(nullptr);
|
||||
return;
|
||||
}
|
||||
SetCursor(LoadCursorW(nullptr, IDC_CROSS));
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
OverlayWindow::OverlayWindow(ScreenCapture&& capture, HWND notificationTarget, OverlayMode mode)
|
||||
@@ -3087,6 +3098,16 @@ LRESULT OverlayWindow::handleMessage(UINT message, WPARAM wParam, LPARAM lParam)
|
||||
}
|
||||
// Panel padding/gaps are UI, not transparent drawing holes.
|
||||
if (s.hitToolbarPanel(p)) return 0;
|
||||
|
||||
if (s.tool != Tool::Drag) {
|
||||
// Drawing/text tools own the entire virtual desktop. The
|
||||
// selection border and resize handles are intentionally
|
||||
// inert until the user explicitly switches back to Drag.
|
||||
s.beginAnnotation(p);
|
||||
s.updateBrushPreview(p);
|
||||
return 0;
|
||||
}
|
||||
|
||||
const int handle = s.hitHandle(p);
|
||||
if (handle >= 0) {
|
||||
// A manual resize commits the currently visible rectangle as
|
||||
@@ -3099,26 +3120,21 @@ LRESULT OverlayWindow::handleMessage(UINT message, WPARAM wParam, LPARAM lParam)
|
||||
SetCapture(s.hwnd);
|
||||
return 0;
|
||||
}
|
||||
if (s.hitSelectionBorder(p) ||
|
||||
(s.tool == Tool::Drag && pointInRect(s.selection, p))) {
|
||||
if (s.hitSelectionBorder(p) || pointInRect(s.selection, p)) {
|
||||
s.draggingSelection = true;
|
||||
s.originalSelection = s.selection;
|
||||
s.originalLogicalSelection = s.logicalSelection;
|
||||
s.appliedSelectionMoveDx = 0;
|
||||
s.appliedSelectionMoveDy = 0;
|
||||
s.dragStart = p;
|
||||
SetCapture(s.hwnd);
|
||||
return 0;
|
||||
}
|
||||
if (pointInRect(s.selection, p)) {
|
||||
s.beginAnnotation(p);
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Drag mode + click outside the crop starts a new crop. Global
|
||||
// annotations are deliberately preserved.
|
||||
const RECT abandonedSelection = s.selection;
|
||||
const RECT abandonedUi = s.uiBounds();
|
||||
const RECT abandonedBadge = s.dimensionBadgeRect;
|
||||
const RECT abandonedPreview = s.brushPreviewRect;
|
||||
s.annotations.clear();
|
||||
s.hasSelection = false;
|
||||
s.selecting = true;
|
||||
s.dragStart = p;
|
||||
@@ -3171,6 +3187,7 @@ LRESULT OverlayWindow::handleMessage(UINT message, WPARAM wParam, LPARAM lParam)
|
||||
s.moveSelectionTo(p);
|
||||
} else if (s.drawing) {
|
||||
s.updateAnnotation(p);
|
||||
s.updateBrushPreview(p);
|
||||
} else {
|
||||
if (!s.hasSelection) {
|
||||
const int nextMonitor = s.monitorIndexForPoint(p);
|
||||
@@ -3220,8 +3237,6 @@ LRESULT OverlayWindow::handleMessage(UINT message, WPARAM wParam, LPARAM lParam)
|
||||
s.invalidateUi();
|
||||
} else if (s.draggingSelection) {
|
||||
s.draggingSelection = false;
|
||||
s.appliedSelectionMoveDx = 0;
|
||||
s.appliedSelectionMoveDy = 0;
|
||||
ReleaseCapture();
|
||||
s.updateLayout();
|
||||
s.brushPreviewRect = s.brushPreviewBounds(p);
|
||||
@@ -3272,7 +3287,7 @@ LRESULT OverlayWindow::handleMessage(UINT message, WPARAM wParam, LPARAM lParam)
|
||||
ScreenToClient(s.hwnd, &point);
|
||||
s.cursorPoint = point;
|
||||
if (!s.paletteOpen && !s.eyedropperActive && s.hasSelection &&
|
||||
pointInRect(s.selection, point) && Impl::isDrawingTool(s.tool)) {
|
||||
Impl::isDrawingTool(s.tool)) {
|
||||
s.adjustCurrentStrokeWidth(GET_WHEEL_DELTA_WPARAM(wParam));
|
||||
s.updateCursor(point);
|
||||
return 0;
|
||||
|
||||
@@ -200,6 +200,11 @@ AppSettings load() {
|
||||
result.arrowWidth = readInt(L"Drawing", L"ArrowWidth", 3, path, 1, 64);
|
||||
result.rectangleWidth = readInt(L"Drawing", L"RectangleWidth", 3, path, 1, 64);
|
||||
result.markerWidth = readInt(L"Drawing", L"MarkerWidth", 15, path, 1, 96);
|
||||
{
|
||||
const std::wstring selected = trim(readString(L"Drawing", L"Color", L"FF3B82F6", path));
|
||||
std::uint32_t parsed = 0xFF3B82F6u;
|
||||
if (parseArgb(selected, parsed)) result.selectedColorArgb = parsed;
|
||||
}
|
||||
|
||||
applyDefaultPalette(result);
|
||||
for (std::size_t i = 0; i < kSavedColorCount; ++i) {
|
||||
@@ -263,6 +268,8 @@ bool save(const AppSettings& value) {
|
||||
ok = writeInt(L"Drawing", L"ArrowWidth", std::clamp(value.arrowWidth, 1, 64), path) && ok;
|
||||
ok = writeInt(L"Drawing", L"RectangleWidth", std::clamp(value.rectangleWidth, 1, 64), path) && ok;
|
||||
ok = writeInt(L"Drawing", L"MarkerWidth", std::clamp(value.markerWidth, 1, 96), path) && ok;
|
||||
ok = WritePrivateProfileStringW(L"Drawing", L"Color",
|
||||
argbText(value.selectedColorArgb).c_str(), path.c_str()) != FALSE && ok;
|
||||
|
||||
for (std::size_t i = 0; i < kSavedColorCount; ++i) {
|
||||
const std::wstring key = paletteKey(i);
|
||||
|
||||
+1
-1
@@ -209,7 +209,7 @@ std::unique_ptr<Result> performUpload(const Job& job) {
|
||||
}
|
||||
|
||||
InternetHandle session(WinHttpOpen(
|
||||
L"NightShot/0.17", WINHTTP_ACCESS_TYPE_AUTOMATIC_PROXY,
|
||||
L"NightShot/0.19", WINHTTP_ACCESS_TYPE_AUTOMATIC_PROXY,
|
||||
WINHTTP_NO_PROXY_NAME, WINHTTP_NO_PROXY_BYPASS, 0));
|
||||
if (!session.value) {
|
||||
result->message = L"WinHttpOpen: " + win32Error(GetLastError());
|
||||
|
||||
Reference in new issue
Block a user