Fix GIF and Overlay issues; Release NightShot v0.19

This commit is contained in:
f1den committed 2026-08-10 16:20:38 +03:00
1 parent dfcbacfb0c
commit ceb0b5552e
10 files changed
+528 -208

No files matched your search

+5 -4
View File
@@ -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
View File
@@ -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();
+3
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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(&center,
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;
+7
View File
@@ -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
View File
@@ -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());