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` - Захват произвольной области экрана через `PrintScreen`
- Поддержка нескольких мониторов и разных значений DPI - Поддержка нескольких мониторов и разных значений DPI без полноразмерного DIB на весь bounding box
- Перемещение и изменение размера выделенной области - Перемещение и изменение размера выделенной области только в режиме Drag
- Аннотации живут на всём виртуальном рабочем столе независимо от области кадрирования
- Карандаш, линии, стрелки, прямоугольники, маркер и текст - Карандаш, линии, стрелки, прямоугольники, маркер и текст
- Настройка толщины инструментов колёсиком мыши - Настройка толщины инструментов колёсиком мыши с постоянным hollow-preview размера кисти
- ColorPicker с пипеткой, HEX, RGBA и HSV - ColorPicker с пипеткой, HEX, RGBA и HSV
- Сохраняемая палитра цветов 8×6 - Сохраняемая палитра цветов 8×6
- Копирование изображения в буфер обмена - Копирование изображения в буфер обмена с явным запросом сохранения в истории Win+V
- Сохранение снимков без перезаписи существующих файлов - Сохранение снимков без перезаписи существующих файлов
- Запись GIF через `Ctrl + PrintScreen` - Запись GIF через `Ctrl + PrintScreen`
- Выбор FPS, качества, курсора и лимита записи - Выбор FPS, качества, курсора и лимита записи
+42 -11
View File
@@ -2,26 +2,57 @@
#include "config.hpp" #include "config.hpp"
#include <windows.h> #include <windows.h>
struct ScreenCapture { #include <cstdint>
#include <vector>
struct ScreenCaptureSurface {
HDC memoryDc = nullptr; HDC memoryDc = nullptr;
HBITMAP bitmap = nullptr; HBITMAP bitmap = nullptr;
HGDIOBJ oldBitmap = nullptr; HGDIOBJ oldBitmap = nullptr;
void* pixels = nullptr; std::uint8_t* pixels = nullptr;
int x = 0; RECT rect{}; // local coordinates inside the virtual-desktop window
int y = 0;
int width = 0; int width = 0;
int height = 0; int height = 0;
int stride = 0; int stride = 0;
ScreenCapture() = default; ScreenCaptureSurface() = default;
ScreenCapture(const ScreenCapture&) = delete; ScreenCaptureSurface(const ScreenCaptureSurface&) = delete;
ScreenCapture& operator=(const ScreenCapture&) = delete; ScreenCaptureSurface& operator=(const ScreenCaptureSurface&) = delete;
ScreenCapture(ScreenCapture&& other) noexcept; ScreenCaptureSurface(ScreenCaptureSurface&& other) noexcept;
ScreenCapture& operator=(ScreenCapture&& other) noexcept; ScreenCaptureSurface& operator=(ScreenCaptureSurface&& other) noexcept;
~ScreenCapture(); ~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; 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(); [[nodiscard]] ScreenCapture captureVirtualDesktop();
+3
View File
@@ -48,6 +48,9 @@ struct AppSettings {
int rectangleWidth = 3; int rectangleWidth = 3;
int markerWidth = 15; 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 // ARGB values. Empty slots remain available for one-click saving from the
// in-editor 8x6 color picker. // in-editor 8x6 color picker.
std::array<std::uint32_t, kSavedColorCount> paletteArgb{}; 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" IDR_APP_MANIFEST RT_MANIFEST "app.manifest"
1 VERSIONINFO 1 VERSIONINFO
FILEVERSION 0,17,0,0 FILEVERSION 0,19,0,0
PRODUCTVERSION 0,17,0,0 PRODUCTVERSION 0,19,0,0
FILEFLAGSMASK 0x3fL FILEFLAGSMASK 0x3fL
FILEFLAGS 0x0L FILEFLAGS 0x0L
FILEOS 0x40004L FILEOS 0x40004L
@@ -17,11 +17,11 @@ BEGIN
BEGIN BEGIN
VALUE "CompanyName", "NightSync" VALUE "CompanyName", "NightSync"
VALUE "FileDescription", "NightShot Win64 screenshot utility" VALUE "FileDescription", "NightShot Win64 screenshot utility"
VALUE "FileVersion", "0.17.0" VALUE "FileVersion", "0.19.0"
VALUE "InternalName", "NightShot" VALUE "InternalName", "NightShot"
VALUE "OriginalFilename", "NightShot.exe" VALUE "OriginalFilename", "NightShot.exe"
VALUE "ProductName", "NightShot" VALUE "ProductName", "NightShot"
VALUE "ProductVersion", "0.17.0" VALUE "ProductVersion", "0.19.0"
END END
END END
BLOCK "VarFileInfo" BLOCK "VarFileInfo"
+170 -27
View File
@@ -1,17 +1,96 @@
#include "capture.hpp" #include "capture.hpp"
#include <algorithm>
#include <cstddef>
#include <utility> #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) { if (this != &other) {
reset(); reset();
memoryDc = std::exchange(other.memoryDc, nullptr); memoryDc = std::exchange(other.memoryDc, nullptr);
bitmap = std::exchange(other.bitmap, nullptr); bitmap = std::exchange(other.bitmap, nullptr);
oldBitmap = std::exchange(other.oldBitmap, nullptr); oldBitmap = std::exchange(other.oldBitmap, nullptr);
pixels = std::exchange(other.pixels, nullptr); pixels = std::exchange(other.pixels, nullptr);
x = std::exchange(other.x, 0); rect = std::exchange(other.rect, RECT{});
y = std::exchange(other.y, 0);
width = std::exchange(other.width, 0); width = std::exchange(other.width, 0);
height = std::exchange(other.height, 0); height = std::exchange(other.height, 0);
stride = std::exchange(other.stride, 0); stride = std::exchange(other.stride, 0);
@@ -19,19 +98,68 @@ ScreenCapture& ScreenCapture::operator=(ScreenCapture&& other) noexcept {
return *this; return *this;
} }
ScreenCapture::~ScreenCapture() { reset(); } ScreenCaptureSurface::~ScreenCaptureSurface() {
reset();
}
void ScreenCapture::reset() noexcept { void ScreenCaptureSurface::reset() noexcept {
if (memoryDc && oldBitmap) { if (memoryDc && oldBitmap && oldBitmap != HGDI_ERROR) {
SelectObject(memoryDc, oldBitmap); SelectObject(memoryDc, oldBitmap);
} }
oldBitmap = nullptr;
if (bitmap) DeleteObject(bitmap); if (bitmap) DeleteObject(bitmap);
bitmap = nullptr;
if (memoryDc) DeleteDC(memoryDc); if (memoryDc) DeleteDC(memoryDc);
memoryDc = nullptr; memoryDc = nullptr;
bitmap = nullptr;
oldBitmap = nullptr;
pixels = 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() { ScreenCapture captureVirtualDesktop() {
@@ -40,30 +168,45 @@ ScreenCapture captureVirtualDesktop() {
result.y = GetSystemMetrics(SM_YVIRTUALSCREEN); result.y = GetSystemMetrics(SM_YVIRTUALSCREEN);
result.width = GetSystemMetrics(SM_CXVIRTUALSCREEN); result.width = GetSystemMetrics(SM_CXVIRTUALSCREEN);
result.height = GetSystemMetrics(SM_CYVIRTUALSCREEN); result.height = GetSystemMetrics(SM_CYVIRTUALSCREEN);
result.stride = result.width * 4;
if (result.width <= 0 || result.height <= 0) return result; if (result.width <= 0 || result.height <= 0) return result;
HDC screenDc = GetDC(nullptr); HDC screenDc = GetDC(nullptr);
if (!screenDc) return result; if (!screenDc) return result;
result.memoryDc = CreateCompatibleDC(screenDc); CaptureContext context{&result, screenDc};
BITMAPINFO info{}; EnumDisplayMonitors(nullptr, nullptr, captureMonitor,
info.bmiHeader.biSize = sizeof(BITMAPINFOHEADER); reinterpret_cast<LPARAM>(&context));
info.bmiHeader.biWidth = result.width;
info.bmiHeader.biHeight = -result.height;
info.bmiHeader.biPlanes = 1;
info.bmiHeader.biBitCount = 32;
info.bmiHeader.biCompression = BI_RGB;
result.bitmap = CreateDIBSection(screenDc, &info, DIB_RGB_COLORS, &result.pixels, nullptr, 0); // Extremely defensive fallback for unusual display drivers which don't
if (result.memoryDc && result.bitmap) { // enumerate monitors through EnumDisplayMonitors.
result.oldBitmap = SelectObject(result.memoryDc, result.bitmap); if (result.surfaces.empty()) {
if (!BitBlt(result.memoryDc, 0, 0, result.width, result.height, screenDc, ScreenCaptureSurface surface;
result.x, result.y, SRCCOPY | CAPTUREBLT)) { surface.width = result.width;
result.reset(); 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); ReleaseDC(nullptr, screenDc);
+131 -35
View File
@@ -15,6 +15,7 @@
#include <atomic> #include <atomic>
#include <chrono> #include <chrono>
#include <condition_variable> #include <condition_variable>
#include <cstddef>
#include <cstdint> #include <cstdint>
#include <cstdio> #include <cstdio>
#include <cwchar> #include <cwchar>
@@ -37,7 +38,7 @@ using Clock = std::chrono::steady_clock;
constexpr wchar_t kHudClass[] = L"NightShotGifRecordingHud"; constexpr wchar_t kHudClass[] = L"NightShotGifRecordingHud";
constexpr COLORREF kTransparentKey = RGB(1, 2, 3); constexpr COLORREF kTransparentKey = RGB(1, 2, 3);
constexpr UINT_PTR kHudTimer = 1; constexpr UINT_PTR kHudTimer = 1;
constexpr std::size_t kMaximumQueuedFrames = 4; constexpr std::size_t kMaximumQueuedFrames = 2;
#ifndef WDA_EXCLUDEFROMCAPTURE #ifndef WDA_EXCLUDEFROMCAPTURE
#define WDA_EXCLUDEFROMCAPTURE 0x00000011 #define WDA_EXCLUDEFROMCAPTURE 0x00000011
#endif #endif
@@ -132,6 +133,9 @@ HGLOBAL globalBytes(const void* data, std::size_t bytes) {
return memory; return memory;
} }
HGLOBAL serializedDword(DWORD value) {
return globalBytes(&value, sizeof(value));
}
HGLOBAL dropFiles(const std::wstring& path) { HGLOBAL dropFiles(const std::wstring& path) {
const std::size_t textBytes = (path.size() + 2) * sizeof(wchar_t); const std::size_t textBytes = (path.size() + 2) * sizeof(wchar_t);
@@ -223,6 +227,29 @@ HGLOBAL firstFrameDib24(const Session& session) {
return memory; 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) { bool openClipboardWithRetry(HWND owner) {
for (int attempt = 0; attempt < 12; ++attempt) { for (int attempt = 0; attempt < 12; ++attempt) {
if (OpenClipboard(owner)) return true; if (OpenClipboard(owner)) return true;
@@ -240,23 +267,30 @@ bool copyGifToClipboard(const Session& session) {
HGLOBAL files = dropFiles(session.path); HGLOBAL files = dropFiles(session.path);
HGLOBAL dibV5 = firstFrameDibV5(session); HGLOBAL dibV5 = firstFrameDibV5(session);
HGLOBAL dib = firstFrameDib24(session); HGLOBAL dib = firstFrameDib24(session);
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 (gifGeneric) GlobalFree(gifGeneric);
if (gifMime) GlobalFree(gifMime); if (gifMime) GlobalFree(gifMime);
if (files) GlobalFree(files); if (files) GlobalFree(files);
if (dibV5) GlobalFree(dibV5); if (dibV5) GlobalFree(dibV5);
if (dib) GlobalFree(dib); if (dib) GlobalFree(dib);
if (bitmap) DeleteObject(bitmap);
if (includeInHistory) GlobalFree(includeInHistory);
};
if (!openClipboardWithRetry(session.request.notificationTarget)) {
cleanup();
return false; return false;
} }
if (!EmptyClipboard()) { if (!EmptyClipboard()) {
CloseClipboard(); CloseClipboard();
if (gifGeneric) GlobalFree(gifGeneric); cleanup();
if (gifMime) GlobalFree(gifMime);
if (files) GlobalFree(files);
if (dibV5) GlobalFree(dibV5);
if (dib) GlobalFree(dib);
return false; return false;
} }
@@ -283,23 +317,96 @@ bool copyGifToClipboard(const Session& session) {
dib = nullptr; dib = nullptr;
transferred = true; transferred = true;
} }
CloseClipboard(); if (bitmap && SetClipboardData(CF_BITMAP, bitmap)) {
bitmap = nullptr;
transferred = true;
}
if (gifGeneric) GlobalFree(gifGeneric); // Explicitly ask Windows 10/11 clipboard monitoring to retain this item in
if (gifMime) GlobalFree(gifMime); // Win+V. The first frame is also published as standard bitmap formats so
if (files) GlobalFree(files); // history has a format it can display even when it ignores the custom GIF.
if (dibV5) GlobalFree(dibV5); const UINT historyFormat = RegisterClipboardFormatW(L"CanIncludeInClipboardHistory");
if (dib) GlobalFree(dib); if (includeInHistory && historyFormat && SetClipboardData(historyFormat, includeInHistory)) {
includeInHistory = nullptr;
}
CloseClipboard();
cleanup();
return transferred; 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) { int paletteColorCount(int quality) {
static constexpr int colors[10]{32, 32, 64, 64, 128, 128, 256, 256, 256, 256}; static constexpr int colors[10]{32, 32, 64, 64, 128, 128, 256, 256, 256, 256};
return colors[std::clamp(quality, 1, 10) - 1]; return colors[std::clamp(quality, 1, 10) - 1];
} }
HRESULT setFrameMetadata(IWICBitmapFrameEncode* frame, UINT16 delay, HRESULT setFrameMetadata(IWICBitmapFrameEncode* frame, UINT16 delay) {
bool firstFrame) {
IWICMetadataQueryWriter* writer = nullptr; IWICMetadataQueryWriter* writer = nullptr;
HRESULT result = frame->GetMetadataQueryWriter(&writer); HRESULT result = frame->GetMetadataQueryWriter(&writer);
if (FAILED(result) || !writer) return FAILED(result) ? result : E_NOINTERFACE; if (FAILED(result) || !writer) return FAILED(result) ? result : E_NOINTERFACE;
@@ -313,9 +420,6 @@ HRESULT setFrameMetadata(IWICBitmapFrameEncode* frame, UINT16 delay,
return result; 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 = PROPVARIANT{};
value.vt = VT_UI1; value.vt = VT_UI1;
value.bVal = 1; value.bVal = 1;
@@ -326,21 +430,6 @@ HRESULT setFrameMetadata(IWICBitmapFrameEncode* frame, UINT16 delay,
value.boolVal = VARIANT_FALSE; value.boolVal = VARIANT_FALSE;
writer->SetMetadataByName(L"/grctlext/TransparencyFlag", &value); 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); releaseCom(writer);
return S_OK; return S_OK;
} }
@@ -358,6 +447,7 @@ public:
width_ = width; width_ = width;
height_ = height; height_ = height;
quality_ = std::clamp(quality, 1, 10); quality_ = std::clamp(quality, 1, 10);
path_ = path;
HRESULT result = CoCreateInstance( HRESULT result = CoCreateInstance(
CLSID_WICImagingFactory, nullptr, CLSCTX_INPROC_SERVER, CLSID_WICImagingFactory, nullptr, CLSCTX_INPROC_SERVER,
@@ -386,6 +476,7 @@ public:
error = L"Не удалось инициализировать GIF encoder: " + hresultText(result); error = L"Не удалось инициализировать GIF encoder: " + hresultText(result);
return false; return false;
} }
(void)setGlobalLoopMetadata(encoder_);
return true; return true;
} }
@@ -473,7 +564,7 @@ public:
} }
if (SUCCEEDED(result)) { if (SUCCEEDED(result)) {
stage = L"GIF metadata"; stage = L"GIF metadata";
result = setFrameMetadata(encodedFrame, delay, frameCount_ == 0); result = setFrameMetadata(encodedFrame, delay);
} }
if (SUCCEEDED(result)) { if (SUCCEEDED(result)) {
// Let WIC convert BGRA directly into the indexed encoder format. // Let WIC convert BGRA directly into the indexed encoder format.
@@ -518,6 +609,10 @@ public:
releaseCom(encoder_); releaseCom(encoder_);
releaseCom(stream_); releaseCom(stream_);
releaseCom(factory_); releaseCom(factory_);
if (!ensureInfiniteLoopExtension(path_)) {
error = L"GIF сохранён, но не удалось гарантировать бесконечное зацикливание.";
return false;
}
return true; return true;
} }
@@ -529,6 +624,7 @@ private:
int height_ = 0; int height_ = 0;
int quality_ = 8; int quality_ = 8;
int frameCount_ = 0; int frameCount_ = 0;
std::wstring path_;
}; };
void drawCursorIntoCapture(HDC destination, const RECT& sourceRect) { void drawCursorIntoCapture(HDC destination, const RECT& sourceRect) {
+25 -1
View File
@@ -222,6 +222,19 @@ bool openClipboardWithRetry(HWND owner) {
} }
return false; 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 } // namespace
bool copyBitmapToClipboard(HWND owner, Gdiplus::Bitmap& bitmap) { bool copyBitmapToClipboard(HWND owner, Gdiplus::Bitmap& bitmap) {
@@ -232,13 +245,18 @@ bool copyBitmapToClipboard(HWND owner, Gdiplus::Bitmap& bitmap) {
HGLOBAL dibV5 = makeDibV5(image); HGLOBAL dibV5 = makeDibV5(image);
HGLOBAL dib = makeDib24(image); HGLOBAL dib = makeDib24(image);
HBITMAP nativeBitmap = makeClipboardBitmap(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 (!openClipboardWithRetry(owner)) {
if (png) GlobalFree(png); if (png) GlobalFree(png);
if (dibV5) GlobalFree(dibV5); if (dibV5) GlobalFree(dibV5);
if (dib) GlobalFree(dib); if (dib) GlobalFree(dib);
if (nativeBitmap) DeleteObject(nativeBitmap); if (nativeBitmap) DeleteObject(nativeBitmap);
if (includeInHistory) GlobalFree(includeInHistory);
return false; return false;
} }
if (!EmptyClipboard()) { if (!EmptyClipboard()) {
@@ -247,6 +265,7 @@ bool copyBitmapToClipboard(HWND owner, Gdiplus::Bitmap& bitmap) {
if (dibV5) GlobalFree(dibV5); if (dibV5) GlobalFree(dibV5);
if (dib) GlobalFree(dib); if (dib) GlobalFree(dib);
if (nativeBitmap) DeleteObject(nativeBitmap); if (nativeBitmap) DeleteObject(nativeBitmap);
if (includeInHistory) GlobalFree(includeInHistory);
return false; return false;
} }
@@ -268,12 +287,17 @@ bool copyBitmapToClipboard(HWND owner, Gdiplus::Bitmap& bitmap) {
nativeBitmap = nullptr; nativeBitmap = nullptr;
transferred = true; transferred = true;
} }
const UINT historyFormat = RegisterClipboardFormatW(L"CanIncludeInClipboardHistory");
if (includeInHistory && historyFormat && SetClipboardData(historyFormat, includeInHistory)) {
includeInHistory = nullptr;
}
CloseClipboard(); CloseClipboard();
if (png) GlobalFree(png); if (png) GlobalFree(png);
if (dibV5) GlobalFree(dibV5); if (dibV5) GlobalFree(dibV5);
if (dib) GlobalFree(dib); if (dib) GlobalFree(dib);
if (nativeBitmap) DeleteObject(nativeBitmap); if (nativeBitmap) DeleteObject(nativeBitmap);
if (includeInHistory) GlobalFree(includeInHistory);
return transferred; 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) { void drawArrow(Graphics& g, const Pen& pen, const Color& color, PointF start, PointF end) {
g.DrawLine(&pen, start, end);
const float dx = end.X - start.X; const float dx = end.X - start.X;
const float dy = end.Y - start.Y; const float dy = end.Y - start.Y;
const float len = std::sqrt(dx * dx + dy * dy); const float length = std::sqrt(dx * dx + dy * dy);
if (len < 1.0f) return; if (length < 1.0f) return;
const float angle = std::atan2(dy, dx);
const float head = std::clamp(len * 0.22f, 9.0f, 18.0f); const float ux = dx / length;
const float spread = 0.55f; const float uy = dy / length;
PointF a(end.X - head * std::cos(angle - spread), end.Y - head * std::sin(angle - spread)); const float nx = -uy;
PointF b(end.X - head * std::cos(angle + spread), end.Y - head * std::sin(angle + spread)); const float ny = ux;
g.DrawLine(&pen, end, a); const float stroke = std::max(1.0f, pen.GetWidth());
g.DrawLine(&pen, end, b); 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) { 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])); if (a.points.size() >= 2) g.DrawLine(&pen, shifted(a.points[0]), shifted(a.points[1]));
break; break;
case Tool::Arrow: 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; break;
case Tool::Rectangle: case Tool::Rectangle:
if (a.points.size() >= 2) { 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); maxY = std::max(maxY, point.Y + offsetY);
} }
float padding = annotation.width * 0.5f + 5.0f; 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) { if (annotation.tool == Tool::Text) {
const float size = std::max(14.0f, annotation.textSize); const float size = std::max(14.0f, annotation.textSize);
maxX += std::max(size * 2.0f, static_cast<float>(annotation.text.size()) * size * 0.72f); 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); markerWidth = static_cast<float>(stored.markerWidth);
savedPalette = stored.paletteArgb; savedPalette = stored.paletteArgb;
savedPaletteOccupied = stored.paletteOccupied; savedPaletteOccupied = stored.paletteOccupied;
color = color_picker_math::colorFromArgb(savedPalette[0]); color = color_picker_math::colorFromArgb(stored.selectedColorArgb);
colorAlpha = color_picker_math::alphaFromArgb(savedPalette[0]); colorAlpha = color_picker_math::alphaFromArgb(stored.selectedColorArgb);
pickerHsv = color_picker_math::rgbToHsv(GetRValue(color), GetGValue(color), GetBValue(color)); pickerHsv = color_picker_math::rgbToHsv(GetRValue(color), GetGValue(color), GetBValue(color));
POINT cursor{}; POINT cursor{};
@@ -600,8 +615,6 @@ struct OverlayWindow::Impl {
Tool tool = Tool::Drag; Tool tool = Tool::Drag;
COLORREF color = theme::kAccent; COLORREF color = theme::kAccent;
std::vector<Annotation> annotations; std::vector<Annotation> annotations;
LONG appliedSelectionMoveDx = 0;
LONG appliedSelectionMoveDy = 0;
std::vector<Button> verticalButtons; std::vector<Button> verticalButtons;
std::vector<Button> horizontalButtons; std::vector<Button> horizontalButtons;
std::array<RECT, 8> handleRects{}; std::array<RECT, 8> handleRects{};
@@ -797,6 +810,7 @@ struct OverlayWindow::Impl {
value.arrowWidth = static_cast<int>(std::lround(arrowWidth)); value.arrowWidth = static_cast<int>(std::lround(arrowWidth));
value.rectangleWidth = static_cast<int>(std::lround(rectangleWidth)); value.rectangleWidth = static_cast<int>(std::lround(rectangleWidth));
value.markerWidth = static_cast<int>(std::lround(markerWidth)); value.markerWidth = static_cast<int>(std::lround(markerWidth));
value.selectedColorArgb = currentArgb();
value.paletteArgb = savedPalette; value.paletteArgb = savedPalette;
value.paletteOccupied = savedPaletteOccupied; value.paletteOccupied = savedPaletteOccupied;
const bool saved = settings::save(value); const bool saved = settings::save(value);
@@ -837,6 +851,7 @@ struct OverlayWindow::Impl {
color = nextColor; color = nextColor;
colorAlpha = nextAlpha; colorAlpha = nextAlpha;
if (syncHsv) syncPickerFromRgb(); if (syncHsv) syncPickerFromRgb();
persistEditorPreferences();
} }
std::uint32_t currentArgb() const { std::uint32_t currentArgb() const {
@@ -929,22 +944,22 @@ struct OverlayWindow::Impl {
changed = true; changed = true;
break; break;
case ColorField::Alpha: 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; changed = true;
break; break;
case ColorField::Hue: case ColorField::Hue:
pickerHsv.h = static_cast<float>((value % 360 + 360) % 360); 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; changed = true;
break; break;
case ColorField::Saturation: case ColorField::Saturation:
pickerHsv.s = static_cast<float>(std::clamp(value, 0, 100)) / 100.0f; 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; changed = true;
break; break;
case ColorField::Value: case ColorField::Value:
pickerHsv.v = static_cast<float>(std::clamp(value, 0, 100)) / 100.0f; 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; changed = true;
break; break;
default: break; default: break;
@@ -1051,19 +1066,19 @@ struct OverlayWindow::Impl {
} }
bool sampleCapturedColor(POINT point) { 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; point.x >= capture.width || point.y >= capture.height) return false;
const auto* row = static_cast<const std::uint8_t*>(capture.pixels) + COLORREF sampled{};
static_cast<std::size_t>(point.y) * static_cast<std::size_t>(capture.stride); if (!capture.samplePixel(point.x, point.y, sampled)) return false;
const auto* pixel = row + static_cast<std::size_t>(point.x) * 4u; setCurrentColor(sampled, 255);
setCurrentColor(RGB(pixel[2], pixel[1], pixel[0]), 255);
return true; return true;
} }
RECT brushPreviewBounds(POINT point) const { RECT brushPreviewBounds(POINT point) const {
if (!hasSelection || !isDrawingTool(tool) || !pointInRect(selection, point)) return RECT{0, 0, 0, 0}; if (!hasSelection || !isDrawingTool(tool) || point.x < 0 || point.y < 0 ||
const int diameter = std::max(3, static_cast<int>(std::ceil(currentStrokeWidth()))); point.x >= capture.width || point.y >= capture.height) return RECT{0, 0, 0, 0};
const int radius = diameter / 2 + 4; 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, return clampToClient(RECT{point.x - radius, point.y - radius,
point.x + radius + 1, point.y + radius + 1}); 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 step = tool == Tool::Marker ? 2.0f : 1.0f;
const float maximum = tool == Tool::Marker ? 96.0f : 64.0f; const float maximum = tool == Tool::Marker ? 96.0f : 64.0f;
const RECT previous = brushPreviewRect; 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(); persistEditorPreferences();
brushPreviewRect = brushPreviewBounds(cursorPoint); brushPreviewRect = brushPreviewBounds(cursorPoint);
invalidateRectPadded(previous, 3); invalidateRectPadded(previous, 3);
@@ -1333,7 +1348,7 @@ struct OverlayWindow::Impl {
maxY = std::max(maxY, point.Y); maxY = std::max(maxY, point.Y);
} }
int padding = static_cast<int>(std::ceil(annotation.width * 0.5f)) + 5; 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) { if (annotation.tool == Tool::Text) {
const float size = std::max(14.0f, annotation.textSize); const float size = std::max(14.0f, annotation.textSize);
maxX += std::max(size * 2.0f, static_cast<float>(annotation.text.size()) * size * 0.72f); maxX += std::max(size * 2.0f, static_cast<float>(annotation.text.size()) * size * 0.72f);
@@ -1726,6 +1741,7 @@ struct OverlayWindow::Impl {
pickerDraggingSv = false; pickerDraggingSv = false;
pickerDraggingHue = false; pickerDraggingHue = false;
pickerDraggingAlpha = false; pickerDraggingAlpha = false;
persistEditorPreferences();
ReleaseCapture(); ReleaseCapture();
} }
@@ -1806,16 +1822,24 @@ struct OverlayWindow::Impl {
std::unique_ptr<Bitmap> renderSelectionBitmap() { std::unique_ptr<Bitmap> renderSelectionBitmap() {
if (!hasSelection || rectWidth(selection) <= 0 || rectHeight(selection) <= 0) return {}; 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()); 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.SetSmoothingMode(SmoothingModeAntiAlias);
g.SetTextRenderingHint(TextRenderingHintAntiAliasGridFit); g.SetTextRenderingHint(TextRenderingHintAntiAliasGridFit);
Bitmap source(capture.bitmap, nullptr); const Rect destination(0, 0, rectWidth(selection), rectHeight(selection));
Rect destination(0, 0, rectWidth(selection), rectHeight(selection));
g.DrawImage(&source, destination, selection.left, selection.top,
rectWidth(selection), rectHeight(selection), UnitPixel);
g.SetClip(destination); 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) for (const auto& annotation : annotations)
drawAnnotation(g, annotation, static_cast<float>(-selection.left), static_cast<float>(-selection.top)); drawAnnotation(g, annotation, static_cast<float>(-selection.left), static_cast<float>(-selection.top));
return output; return output;
@@ -2418,44 +2442,35 @@ struct OverlayWindow::Impl {
} }
void drawBrushPreview(Graphics& g, RECT dirty) { void drawBrushPreview(Graphics& g, RECT dirty) {
if (!hasSelection || selecting || drawing || draggingSelection || resizeHandle >= 0 || if (!hasSelection || selecting || draggingSelection || resizeHandle >= 0 ||
paletteOpen || eyedropperActive || !isDrawingTool(tool) || paletteOpen || eyedropperActive || !isDrawingTool(tool) ||
!rectsIntersect(brushPreviewRect, dirty) || !rectsIntersect(brushPreviewRect, dirty)) return;
!pointInRect(selection, cursorPoint)) return;
const float width = std::max(1.0f, currentStrokeWidth()); const float diameter = std::max(1.0f, currentStrokeWidth());
const float visualDiameter = std::max(4.0f, width); const float x = static_cast<float>(cursorPoint.x) - diameter * 0.5f;
const float x = static_cast<float>(cursorPoint.x) - visualDiameter * 0.5f; const float y = static_cast<float>(cursorPoint.y) - diameter * 0.5f;
const float y = static_cast<float>(cursorPoint.y) - visualDiameter * 0.5f;
const GraphicsState state = g.Save(); const GraphicsState state = g.Save();
g.SetSmoothingMode(SmoothingModeAntiAlias); g.SetSmoothingMode(SmoothingModeAntiAlias);
g.SetPixelOffsetMode(PixelOffsetModeHalf); 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))); // Preview is deliberately hollow: it represents the exact physical
g.FillEllipse(&fill, x, y, visualDiameter, visualDiameter); // brush diameter without covering the pixels the user is trying to aim
Pen shadow(Color(205, 0, 0, 0), 3.0f); // at. Keep the border at one physical pixel on every monitor.
Pen border(Color(245, 255, 255, 255), 1.0f); Pen border(toGdiColor(color, static_cast<BYTE>(std::max<int>(colorAlpha, 160))), 1.0f);
g.DrawEllipse(&shadow, x, y, visualDiameter, visualDiameter); g.DrawEllipse(&border, x, y, diameter, diameter);
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);
}
g.Restore(state); g.Restore(state);
} }
void drawScene(Graphics& g, RECT dirty) { 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) { if (hasSelection || selecting) {
const RECT r = selection; 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 int borderPadding = std::max(10, ui.handle + 6);
const std::array<RECT, 4> borderBands{{ 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, rectWidth(r) - 1)),
static_cast<REAL>(std::max(0, rectHeight(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)); SolidBrush handleFill(toGdiColor(theme::kAccentHover));
Pen handleBorder(Color(210, 255, 255, 255), 1.0f); Pen handleBorder(Color(210, 255, 255, 255), 1.0f);
for (const RECT& handle : handleRects) { for (const RECT& handle : handleRects) {
@@ -2597,7 +2612,7 @@ struct OverlayWindow::Impl {
const int height = rectHeight(tile); const int height = rectHeight(tile);
if (width <= 0 || height <= 0 || !ensureScratchBuffer(target, width, height)) return; 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); dimScratchRect(tile);
Graphics graphics(scratchDc); Graphics graphics(scratchDc);
@@ -2666,11 +2681,11 @@ struct OverlayWindow::Impl {
const int padding = static_cast<int>(std::ceil(annotation.width * 0.5f)) + 4; 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) { if ((annotation.tool == Tool::Line || annotation.tool == Tool::Arrow) && annotation.points.size() >= 2) {
invalidateSegmentTiles(annotation.points[0], annotation.points[1], 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) { if (annotation.tool == Tool::Arrow) {
const PointF end = annotation.points[1]; const PointF end = annotation.points[1];
invalidateRectPadded(RECT{static_cast<LONG>(end.X) - 24, static_cast<LONG>(end.Y) - 24, invalidateRectPadded(RECT{static_cast<LONG>(end.X) - 36, static_cast<LONG>(end.Y) - 36,
static_cast<LONG>(end.X) + 25, static_cast<LONG>(end.Y) + 25}, 1); static_cast<LONG>(end.X) + 37, static_cast<LONG>(end.Y) + 37}, 1);
} }
} else if (annotation.tool == Tool::Rectangle && annotation.points.size() >= 2) { } else if (annotation.tool == Tool::Rectangle && annotation.points.size() >= 2) {
invalidateRectangleOutline(annotation.points[0], annotation.points[1], padding); invalidateRectangleOutline(annotation.points[0], annotation.points[1], padding);
@@ -2680,7 +2695,9 @@ struct OverlayWindow::Impl {
} }
void beginAnnotation(POINT p) { 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) { if (tool == Tool::Text) {
beginTextEdit(p); beginTextEdit(p);
return; return;
@@ -2701,8 +2718,8 @@ struct OverlayWindow::Impl {
void updateAnnotation(POINT p) { void updateAnnotation(POINT p) {
if (!drawing || annotations.empty()) return; if (!drawing || annotations.empty()) return;
p.x = std::clamp(p.x, selection.left, selection.right); p.x = std::clamp(p.x, 0L, static_cast<LONG>(capture.width - 1));
p.y = std::clamp(p.y, selection.top, selection.bottom); p.y = std::clamp(p.y, 0L, static_cast<LONG>(capture.height - 1));
Annotation& annotation = annotations.back(); Annotation& annotation = annotations.back();
if (annotation.tool == Tool::Pen || annotation.tool == Tool::Marker) { if (annotation.tool == Tool::Pen || annotation.tool == Tool::Marker) {
@@ -2746,7 +2763,7 @@ struct OverlayWindow::Impl {
if (!textEditing) return; if (!textEditing) return;
const int monitorIndex = monitorIndexForPoint(textAnchor); const int monitorIndex = monitorIndexForPoint(textAnchor);
const UiMetrics metrics = metricsForMonitor(monitorIndex); const UiMetrics metrics = metricsForMonitor(monitorIndex);
RECT safe = intersectRects(monitors[monitorIndex].workBounds, selection); RECT safe = monitors[monitorIndex].workBounds;
RECT insetSafe = safe; RECT insetSafe = safe;
InflateRect(&insetSafe, -metrics.margin, -metrics.margin); InflateRect(&insetSafe, -metrics.margin, -metrics.margin);
if (rectWidth(insetSafe) >= 40 && rectHeight(insetSafe) >= 24) safe = insetSafe; if (rectWidth(insetSafe) >= 40 && rectHeight(insetSafe) >= 24) safe = insetSafe;
@@ -2779,7 +2796,7 @@ struct OverlayWindow::Impl {
commitTextEdit(); commitTextEdit();
const int monitorIndex = monitorIndexForPoint(p); const int monitorIndex = monitorIndexForPoint(p);
const UiMetrics metrics = metricsForMonitor(monitorIndex); const UiMetrics metrics = metricsForMonitor(monitorIndex);
RECT available = intersectRects(monitors[monitorIndex].workBounds, selection); RECT available = monitors[monitorIndex].workBounds;
InflateRect(&available, -metrics.margin, -metrics.margin); InflateRect(&available, -metrics.margin, -metrics.margin);
if (rectWidth(available) < 50 || rectHeight(available) < 32) return; if (rectWidth(available) < 50 || rectHeight(available) < 32) return;
@@ -2900,9 +2917,6 @@ struct OverlayWindow::Impl {
const RECT previousUi = uiBounds(); const RECT previousUi = uiBounds();
const RECT previousBadge = dimensionBadgeRect; const RECT previousBadge = dimensionBadgeRect;
const RECT previousPreview = brushPreviewRect; const RECT previousPreview = brushPreviewRect;
if (!annotations.empty()) {
for (const auto& annotation : annotations) invalidateAnnotationShape(annotation);
}
const LONG requestedDx = p.x - dragStart.x; const LONG requestedDx = p.x - dragStart.x;
const LONG requestedDy = p.y - dragStart.y; const LONG requestedDy = p.y - dragStart.y;
@@ -2910,29 +2924,11 @@ struct OverlayWindow::Impl {
OffsetRect(&nextLogical, requestedDx, requestedDy); OffsetRect(&nextLogical, requestedDx, requestedDy);
nextLogical = keepLogicalSelectionReachable(nextLogical); nextLogical = keepLogicalSelectionReachable(nextLogical);
const LONG actualDx = nextLogical.left - originalLogicalSelection.left;
const LONG actualDy = nextLogical.top - originalLogicalSelection.top;
logicalSelection = nextLogical; logicalSelection = nextLogical;
selection = visibleSelectionFromLogical(logicalSelection); selection = visibleSelectionFromLogical(logicalSelection);
const LONG deltaDx = actualDx - appliedSelectionMoveDx; // Annotations deliberately stay in absolute virtual-desktop
const LONG deltaDy = actualDy - appliedSelectionMoveDy; // coordinates. Selection is only the crop rectangle.
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);
}
updateLayout(); updateLayout();
brushPreviewRect = brushPreviewBounds(cursorPoint); brushPreviewRect = brushPreviewBounds(cursorPoint);
invalidateSelectionTransition(previousSelection, previousUi, previousBadge); invalidateSelectionTransition(previousSelection, previousUi, previousBadge);
@@ -2955,26 +2951,41 @@ struct OverlayWindow::Impl {
if (pointInRect(pickerEyedropperRect, p)) { SetCursor(LoadCursorW(nullptr, IDC_HAND)); return; } if (pointInRect(pickerEyedropperRect, p)) { SetCursor(LoadCursorW(nullptr, IDC_HAND)); return; }
} }
if (!hasSelection) { SetCursor(LoadCursorW(nullptr, IDC_CROSS)); 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 (hitButton(p) >= 0 || hitPalette(p)) { SetCursor(LoadCursorW(nullptr, IDC_HAND)); return; }
if (hitToolbarPanel(p)) { SetCursor(LoadCursorW(nullptr, IDC_ARROW)); return; } if (hitToolbarPanel(p)) { SetCursor(LoadCursorW(nullptr, IDC_ARROW)); return; }
if (hitSelectionBorder(p)) { SetCursor(LoadCursorW(nullptr, IDC_SIZEALL)); return; }
if (pointInRect(selection, p)) { // The selection is interactive only in Drag mode. Drawing tools never
if (tool == Tool::Drag) SetCursor(LoadCursorW(nullptr, IDC_SIZEALL)); // accidentally resize/move the crop when a stroke starts near its edge.
else if (tool == Tool::Text) SetCursor(LoadCursorW(nullptr, IDC_IBEAM)); if (tool == Tool::Drag) {
else if (isDrawingTool(tool)) SetCursor(nullptr); const int handle = hitHandle(p);
else SetCursor(LoadCursorW(nullptr, IDC_CROSS)); 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; 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) 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. // Panel padding/gaps are UI, not transparent drawing holes.
if (s.hitToolbarPanel(p)) return 0; 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); const int handle = s.hitHandle(p);
if (handle >= 0) { if (handle >= 0) {
// A manual resize commits the currently visible rectangle as // 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); SetCapture(s.hwnd);
return 0; return 0;
} }
if (s.hitSelectionBorder(p) || if (s.hitSelectionBorder(p) || pointInRect(s.selection, p)) {
(s.tool == Tool::Drag && pointInRect(s.selection, p))) {
s.draggingSelection = true; s.draggingSelection = true;
s.originalSelection = s.selection; s.originalSelection = s.selection;
s.originalLogicalSelection = s.logicalSelection; s.originalLogicalSelection = s.logicalSelection;
s.appliedSelectionMoveDx = 0;
s.appliedSelectionMoveDy = 0;
s.dragStart = p; s.dragStart = p;
SetCapture(s.hwnd); SetCapture(s.hwnd);
return 0; return 0;
} }
if (pointInRect(s.selection, p)) {
s.beginAnnotation(p); // Drag mode + click outside the crop starts a new crop. Global
return 0; // annotations are deliberately preserved.
}
const RECT abandonedSelection = s.selection; const RECT abandonedSelection = s.selection;
const RECT abandonedUi = s.uiBounds(); const RECT abandonedUi = s.uiBounds();
const RECT abandonedBadge = s.dimensionBadgeRect; const RECT abandonedBadge = s.dimensionBadgeRect;
const RECT abandonedPreview = s.brushPreviewRect; const RECT abandonedPreview = s.brushPreviewRect;
s.annotations.clear();
s.hasSelection = false; s.hasSelection = false;
s.selecting = true; s.selecting = true;
s.dragStart = p; s.dragStart = p;
@@ -3171,6 +3187,7 @@ LRESULT OverlayWindow::handleMessage(UINT message, WPARAM wParam, LPARAM lParam)
s.moveSelectionTo(p); s.moveSelectionTo(p);
} else if (s.drawing) { } else if (s.drawing) {
s.updateAnnotation(p); s.updateAnnotation(p);
s.updateBrushPreview(p);
} else { } else {
if (!s.hasSelection) { if (!s.hasSelection) {
const int nextMonitor = s.monitorIndexForPoint(p); const int nextMonitor = s.monitorIndexForPoint(p);
@@ -3220,8 +3237,6 @@ LRESULT OverlayWindow::handleMessage(UINT message, WPARAM wParam, LPARAM lParam)
s.invalidateUi(); s.invalidateUi();
} else if (s.draggingSelection) { } else if (s.draggingSelection) {
s.draggingSelection = false; s.draggingSelection = false;
s.appliedSelectionMoveDx = 0;
s.appliedSelectionMoveDy = 0;
ReleaseCapture(); ReleaseCapture();
s.updateLayout(); s.updateLayout();
s.brushPreviewRect = s.brushPreviewBounds(p); s.brushPreviewRect = s.brushPreviewBounds(p);
@@ -3272,7 +3287,7 @@ LRESULT OverlayWindow::handleMessage(UINT message, WPARAM wParam, LPARAM lParam)
ScreenToClient(s.hwnd, &point); ScreenToClient(s.hwnd, &point);
s.cursorPoint = point; s.cursorPoint = point;
if (!s.paletteOpen && !s.eyedropperActive && s.hasSelection && 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.adjustCurrentStrokeWidth(GET_WHEEL_DELTA_WPARAM(wParam));
s.updateCursor(point); s.updateCursor(point);
return 0; return 0;
+7
View File
@@ -200,6 +200,11 @@ AppSettings load() {
result.arrowWidth = readInt(L"Drawing", L"ArrowWidth", 3, path, 1, 64); result.arrowWidth = readInt(L"Drawing", L"ArrowWidth", 3, path, 1, 64);
result.rectangleWidth = readInt(L"Drawing", L"RectangleWidth", 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); 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); applyDefaultPalette(result);
for (std::size_t i = 0; i < kSavedColorCount; ++i) { 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"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"RectangleWidth", std::clamp(value.rectangleWidth, 1, 64), path) && ok;
ok = writeInt(L"Drawing", L"MarkerWidth", std::clamp(value.markerWidth, 1, 96), 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) { for (std::size_t i = 0; i < kSavedColorCount; ++i) {
const std::wstring key = paletteKey(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( 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)); WINHTTP_NO_PROXY_NAME, WINHTTP_NO_PROXY_BYPASS, 0));
if (!session.value) { if (!session.value) {
result->message = L"WinHttpOpen: " + win32Error(GetLastError()); result->message = L"WinHttpOpen: " + win32Error(GetLastError());